marvin 0.8.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/marvin +33 -0
- data/handlers/debug_handler.rb +5 -0
- data/handlers/hello_world.rb +9 -0
- data/handlers/keiki_thwopper.rb +21 -0
- data/handlers/simple_logger.rb +24 -0
- data/handlers/tweet_tweet.rb +19 -0
- data/lib/marvin.rb +56 -0
- data/lib/marvin/abstract_client.rb +146 -0
- data/lib/marvin/abstract_parser.rb +29 -0
- data/lib/marvin/base.rb +195 -0
- data/lib/marvin/client/actions.rb +104 -0
- data/lib/marvin/client/default_handlers.rb +97 -0
- data/lib/marvin/command_handler.rb +91 -0
- data/lib/marvin/console.rb +50 -0
- data/lib/marvin/core_commands.rb +49 -0
- data/lib/marvin/distributed.rb +8 -0
- data/lib/marvin/distributed/client.rb +225 -0
- data/lib/marvin/distributed/handler.rb +85 -0
- data/lib/marvin/distributed/protocol.rb +88 -0
- data/lib/marvin/distributed/server.rb +154 -0
- data/lib/marvin/dsl.rb +103 -0
- data/lib/marvin/exception_tracker.rb +19 -0
- data/lib/marvin/exceptions.rb +11 -0
- data/lib/marvin/irc.rb +7 -0
- data/lib/marvin/irc/client.rb +168 -0
- data/lib/marvin/irc/event.rb +39 -0
- data/lib/marvin/irc/replies.rb +154 -0
- data/lib/marvin/logging_handler.rb +76 -0
- data/lib/marvin/middle_man.rb +103 -0
- data/lib/marvin/parsers.rb +9 -0
- data/lib/marvin/parsers/command.rb +107 -0
- data/lib/marvin/parsers/prefixes.rb +8 -0
- data/lib/marvin/parsers/prefixes/host_mask.rb +35 -0
- data/lib/marvin/parsers/prefixes/server.rb +24 -0
- data/lib/marvin/parsers/ragel_parser.rb +720 -0
- data/lib/marvin/parsers/ragel_parser.rl +143 -0
- data/lib/marvin/parsers/simple_parser.rb +35 -0
- data/lib/marvin/settings.rb +31 -0
- data/lib/marvin/test_client.rb +58 -0
- data/lib/marvin/util.rb +54 -0
- data/templates/boot.erb +3 -0
- data/templates/connections.yml.erb +10 -0
- data/templates/debug_handler.erb +5 -0
- data/templates/hello_world.erb +10 -0
- data/templates/rakefile.erb +15 -0
- data/templates/settings.yml.erb +8 -0
- data/templates/setup.erb +31 -0
- data/templates/test_helper.erb +17 -0
- data/test/abstract_client_test.rb +63 -0
- data/test/parser_comparison.rb +62 -0
- data/test/parser_test.rb +266 -0
- data/test/test_helper.rb +62 -0
- data/test/util_test.rb +57 -0
- metadata +136 -0
@@ -0,0 +1,143 @@
|
|
1
|
+
# Ragel Parser comes from the Arrbot Guys - Kudos to Halogrium and Epitron.
|
2
|
+
|
3
|
+
%%{
|
4
|
+
machine irc;
|
5
|
+
|
6
|
+
action prefix_servername_start {
|
7
|
+
server = Prefixes::Server.new
|
8
|
+
}
|
9
|
+
|
10
|
+
action prefix_servername {
|
11
|
+
server.name << fc
|
12
|
+
}
|
13
|
+
|
14
|
+
action prefix_servername_finish {
|
15
|
+
command.prefix = server
|
16
|
+
}
|
17
|
+
|
18
|
+
action prefix_hostmask_start {
|
19
|
+
hostmask = Prefixes::HostMask.new
|
20
|
+
}
|
21
|
+
|
22
|
+
action hostmask_nickname {
|
23
|
+
hostmask.nick << fc
|
24
|
+
}
|
25
|
+
|
26
|
+
action hostmask_user {
|
27
|
+
hostmask.user << fc
|
28
|
+
}
|
29
|
+
|
30
|
+
action hostmask_host {
|
31
|
+
hostmask.host << fc
|
32
|
+
}
|
33
|
+
|
34
|
+
action prefix_hostmask_finish {
|
35
|
+
command.prefix = hostmask
|
36
|
+
}
|
37
|
+
|
38
|
+
action message_code_start {
|
39
|
+
code = ""
|
40
|
+
}
|
41
|
+
|
42
|
+
action message_code {
|
43
|
+
code << fc
|
44
|
+
}
|
45
|
+
|
46
|
+
action message_code_finish {
|
47
|
+
command.code = code
|
48
|
+
}
|
49
|
+
|
50
|
+
action params_start {
|
51
|
+
params_1 = []
|
52
|
+
params_2 = []
|
53
|
+
}
|
54
|
+
|
55
|
+
action params {
|
56
|
+
}
|
57
|
+
|
58
|
+
action params_1_start {
|
59
|
+
params_1 << ""
|
60
|
+
}
|
61
|
+
|
62
|
+
action params_2_start {
|
63
|
+
params_2 << ""
|
64
|
+
}
|
65
|
+
|
66
|
+
action params_1 {
|
67
|
+
params_1.last << fc
|
68
|
+
}
|
69
|
+
|
70
|
+
action params_2 {
|
71
|
+
params_2.last << fc
|
72
|
+
}
|
73
|
+
|
74
|
+
action params_1_finish {
|
75
|
+
command.params = params_1
|
76
|
+
}
|
77
|
+
|
78
|
+
action params_2_finish {
|
79
|
+
command.params = params_2
|
80
|
+
}
|
81
|
+
|
82
|
+
SPACE = " ";
|
83
|
+
special = "[" | "\\" | "]" | "^" | "_" | "`" | "{" | "|" | "}" | "+";
|
84
|
+
nospcrlfcl = extend - ( 0 | SPACE | '\r' | '\n' | ':' );
|
85
|
+
crlf = "\r\n";
|
86
|
+
shortname = ( alnum ( alnum | "-" )* alnum* ) | "*";
|
87
|
+
multihostname = shortname ( ( "." | "/" ) shortname )*;
|
88
|
+
singlehostname = shortname ( "." | "/" );
|
89
|
+
hostname = multihostname | singlehostname;
|
90
|
+
servername = hostname;
|
91
|
+
nickname = ( alpha | special ) ( alnum | special | "-" ){,15};
|
92
|
+
user = (extend - ( 0 | "\n" | "\r" | SPACE | "@" ))+;
|
93
|
+
ip4addr = digit{1,3} "." digit{1,3} "." digit{1,3} "." digit{1,3};
|
94
|
+
ip6addr = ( xdigit+ ( ":" xdigit+ ){7} ) | ( "0:0:0:0:0:" ( "0" | "FFFF"i ) ":" ip4addr );
|
95
|
+
hostaddr = ip4addr | ip6addr;
|
96
|
+
host = hostname | hostaddr;
|
97
|
+
hostmask = nickname $ hostmask_nickname ( ( "!" user $ hostmask_user )? "@" host $ hostmask_host )?;
|
98
|
+
prefix = ( servername $ prefix_servername > prefix_servername_start % prefix_servername_finish ) | ( hostmask > prefix_hostmask_start % prefix_hostmask_finish );
|
99
|
+
code = alpha+ | digit{3};
|
100
|
+
middle = nospcrlfcl ( ":" | nospcrlfcl )*;
|
101
|
+
trailing = ( ":" | " " | nospcrlfcl )*;
|
102
|
+
params_1 = ( SPACE middle $ params_1 > params_1_start ){,14} ( SPACE ":" trailing $ params_1 > params_1_start )?;
|
103
|
+
params_2 = ( SPACE middle $ params_2 > params_2_start ){14} ( SPACE ":"? trailing $ params_2 > params_2_start )?;
|
104
|
+
params = ( params_1 % params_1_finish | params_2 % params_2_finish ) $ params > params_start;
|
105
|
+
message = ( ":" prefix SPACE )? ( code $ message_code > message_code_start % message_code_finish ) params? crlf;
|
106
|
+
|
107
|
+
main := message;
|
108
|
+
|
109
|
+
}%%
|
110
|
+
|
111
|
+
module Marvin
|
112
|
+
module Parsers
|
113
|
+
class RagelParser < Marvin::AbstractParser
|
114
|
+
|
115
|
+
%% write data;
|
116
|
+
|
117
|
+
private
|
118
|
+
|
119
|
+
def self.parse!(line)
|
120
|
+
data = "#{line.strip}\r\n"
|
121
|
+
|
122
|
+
p = 0;
|
123
|
+
pe = data.length
|
124
|
+
cs = 0
|
125
|
+
|
126
|
+
hostmask = nil
|
127
|
+
server = nil
|
128
|
+
code = nil
|
129
|
+
command = Command.new(data)
|
130
|
+
|
131
|
+
%% write init;
|
132
|
+
%% write exec;
|
133
|
+
|
134
|
+
if cs >= irc_first_final
|
135
|
+
command
|
136
|
+
else
|
137
|
+
raise UnparseableMessage, "Failed to parse the message: #{line.strip}"
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Marvin
|
2
|
+
module Parsers
|
3
|
+
class SimpleParser < Marvin::AbstractParser
|
4
|
+
|
5
|
+
private
|
6
|
+
|
7
|
+
# Parses an incoming message by using string
|
8
|
+
# Manipulation.
|
9
|
+
def self.parse!(line)
|
10
|
+
prefix_text = nil
|
11
|
+
prefix_text, line = line.split(" ", 2) if line[0] == ?:
|
12
|
+
command = Command.new("#{line}\r\n")
|
13
|
+
command.prefix = self.extract_prefix(prefix_text)
|
14
|
+
parts = Marvin::Util.arguments(line)
|
15
|
+
command.code = parts.shift
|
16
|
+
command.params = parts
|
17
|
+
return command
|
18
|
+
end
|
19
|
+
|
20
|
+
# From a given string, attempts to get the correct
|
21
|
+
# type of prefix (be it a HostMask or a Server name).
|
22
|
+
def self.extract_prefix(prefix_text)
|
23
|
+
return if prefix_text.blank?
|
24
|
+
prefix_text = prefix_text[1..-1] # Remove the leading :
|
25
|
+
# I think I just vomitted in my mouth a little...
|
26
|
+
if prefix_text =~ /^([A-Za-z0-9\-\[\]\\\`\^\|\{\}\_]+)(\!\~?([^@]+))?(@(.*))?$/
|
27
|
+
prefix = Prefixes::HostMask.new($1, $3, $5)
|
28
|
+
else
|
29
|
+
prefix = Prefixes::Server.new(prefix_text.strip)
|
30
|
+
end
|
31
|
+
return prefix
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# Example of extending built in Perennial
|
2
|
+
# functionality. Since const_get etc acts
|
3
|
+
# oddly, this is the best way to do it.
|
4
|
+
Marvin::Settings.class_eval do
|
5
|
+
|
6
|
+
def self.parser
|
7
|
+
# We use SimpleParser by default because it is almost
|
8
|
+
# 20 times faster (from basic benchmarks) than the Ragel
|
9
|
+
# based parser. If you're having issues with unexpected
|
10
|
+
# results, please try using Ragel as the parser for you
|
11
|
+
# application - It was (afaik) almost a direct port
|
12
|
+
# from the RFC where as I've taken some liberties with
|
13
|
+
# simple parser for the expected reasons.
|
14
|
+
@@parser ||= Marvin::Parsers::SimpleParser
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.parser=(value)
|
18
|
+
raise ArgumentError, 'Is not a valid parser implementation' unless value < Marvin::AbstractParser
|
19
|
+
@@parser = value
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.client
|
23
|
+
@@client ||= Marvin::IRC::Client
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.client=(value)
|
27
|
+
raise ArgumentError, 'Is not a valid client implementation' unless value < Marvin::AbstractClient
|
28
|
+
@@client = value
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Marvin
|
2
|
+
# Marvin::TestClient is a simple client used for testing
|
3
|
+
# Marvin::Base derivatives in a non-network-reliant setting.
|
4
|
+
class TestClient < AbstractClient
|
5
|
+
|
6
|
+
attr_accessor :incoming_commands, :outgoing_commands, :last_sent,
|
7
|
+
:dispatched_events, :connection_open
|
8
|
+
|
9
|
+
cattr_accessor :instances
|
10
|
+
@@instances = []
|
11
|
+
|
12
|
+
DispatchedEvents = Struct.new(:name, :options)
|
13
|
+
|
14
|
+
def initialize(opts = {})
|
15
|
+
super
|
16
|
+
@incoming_commands = []
|
17
|
+
@outgoing_commands = []
|
18
|
+
@dispatched_events = []
|
19
|
+
@connection_open = false
|
20
|
+
@@instances << self
|
21
|
+
end
|
22
|
+
|
23
|
+
def connection_open?
|
24
|
+
!!@connection_open
|
25
|
+
end
|
26
|
+
|
27
|
+
def send_line(*args)
|
28
|
+
@outgoing_commands += args
|
29
|
+
@last_sent = args.last
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_command(name, *args)
|
33
|
+
options = args.extract_options!
|
34
|
+
host_mask = options.delete(:host_mask) || ":WiZ!jto@tolsun.oulu.fi"
|
35
|
+
name = name.to_s.upcase
|
36
|
+
args = args.flatten.compact
|
37
|
+
receive_line "#{host_mask} #{name} #{args.join(" ").strip}"
|
38
|
+
end
|
39
|
+
|
40
|
+
def dispatch(name, opts = {})
|
41
|
+
@dispatched_events << [name, opts]
|
42
|
+
super(name, opts)
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.run
|
46
|
+
@@instances.each { |i| i.connection_open = true }
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.stop
|
50
|
+
@@instances.each { |i| i.connection_open = false }
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.add_reconnect(opts = {})
|
54
|
+
logger.info "Added reconnect with options: #{opts.inspect}"
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
end
|
data/lib/marvin/util.rb
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
module Marvin
|
2
|
+
module Util
|
3
|
+
|
4
|
+
GLOB_PATTERN_MAP = {
|
5
|
+
'*' => '.*',
|
6
|
+
'?' => '.',
|
7
|
+
'[' => '[',
|
8
|
+
']' => ']'
|
9
|
+
}
|
10
|
+
|
11
|
+
# Return the channel-name version of a string by
|
12
|
+
# appending "#" to the front if it doesn't already
|
13
|
+
# start with it.
|
14
|
+
def channel_name(name)
|
15
|
+
name = name.to_s
|
16
|
+
name =~ /^\#/ ? name : "##{name}"
|
17
|
+
end
|
18
|
+
alias chan channel_name
|
19
|
+
|
20
|
+
def arguments(input)
|
21
|
+
prefix, ending = input.split(":", 2)
|
22
|
+
prefix = prefix.split(" ")
|
23
|
+
prefix << ending unless ending.blank?
|
24
|
+
return prefix
|
25
|
+
end
|
26
|
+
|
27
|
+
# Specifies the last parameter of a response, used to
|
28
|
+
# specify parameters which have spaces etc (for example,
|
29
|
+
# the actual message part of a response).
|
30
|
+
def last_param(section, ignore_prefix = true)
|
31
|
+
content = section.to_s.strip
|
32
|
+
return if content.blank?
|
33
|
+
if content =~ /\s+/ && (ignore_prefix || content !~ /^:/)
|
34
|
+
":#{content}"
|
35
|
+
else
|
36
|
+
content
|
37
|
+
end
|
38
|
+
end
|
39
|
+
alias lp last_param
|
40
|
+
|
41
|
+
# Converts a glob-like pattern into a regular
|
42
|
+
# expression for easy / fast matching. Code is
|
43
|
+
# from PLEAC at http://pleac.sourceforge.net/pleac_ruby/patternmatching.html
|
44
|
+
def glob2pattern(glob_string)
|
45
|
+
inner_pattern = glob_string.gsub(/(.)/) do |c|
|
46
|
+
GLOB_PATTERN_MAP[c] || Regexp::escape(c)
|
47
|
+
end
|
48
|
+
return Regexp.new("^#{inner_pattern}$")
|
49
|
+
end
|
50
|
+
|
51
|
+
extend self
|
52
|
+
|
53
|
+
end
|
54
|
+
end
|
data/templates/boot.erb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/testtask'
|
3
|
+
|
4
|
+
task :default => "test:units"
|
5
|
+
|
6
|
+
namespace :test do
|
7
|
+
|
8
|
+
desc "Runs the unit tests for perennial"
|
9
|
+
Rake::TestTask.new("units") do |t|
|
10
|
+
t.pattern = 'test/*_test.rb'
|
11
|
+
t.libs << 'test'
|
12
|
+
t.verbose = true
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
data/templates/setup.erb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# Is loaded on setup / when handlers need to be
|
2
|
+
# registered. Use it to register handlers / do
|
3
|
+
# any repeatable setup that will happen before
|
4
|
+
# any connections are created
|
5
|
+
Marvin::Loader.before_run do
|
6
|
+
|
7
|
+
# E.G.
|
8
|
+
# MyHandler.register! (Marvin::Base subclass) or
|
9
|
+
# Marvin::Settings.client.register_handler my_handler (a handler instance)
|
10
|
+
|
11
|
+
# Register based on some setting you've added. e.g.:
|
12
|
+
# LoggingHandler.register! if Marvin::Settings.use_logging?
|
13
|
+
|
14
|
+
# Conditional registration - load the distributed dispatcher
|
15
|
+
# if an actual client, otherwise use the normal handlers.
|
16
|
+
#
|
17
|
+
# if Marvin::Loader.distributed_client?
|
18
|
+
# HelloWorld.register!
|
19
|
+
# DebugHandler.register!
|
20
|
+
# else
|
21
|
+
# Marvin::Distributed::Handler.register!
|
22
|
+
# else
|
23
|
+
|
24
|
+
# end
|
25
|
+
|
26
|
+
# And any other code here that will be run before the client, e.g:
|
27
|
+
|
28
|
+
HelloWorld.register!
|
29
|
+
DebugHandler.register!
|
30
|
+
|
31
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
|
3
|
+
# Testing dependencies
|
4
|
+
require 'test/unit'
|
5
|
+
require 'shoulda'
|
6
|
+
# RedGreen doesn't seem to be needed under 1.9
|
7
|
+
require 'redgreen' if RUBY_VERSION < "1.9"
|
8
|
+
|
9
|
+
require 'pathname'
|
10
|
+
root_directory = Pathname.new(__FILE__).dirname.join("..").expand_path
|
11
|
+
require root_directory.join("config", "boot")
|
12
|
+
|
13
|
+
class Test::Unit::TestCase
|
14
|
+
|
15
|
+
# Add your extensions here
|
16
|
+
|
17
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'test_helper')
|
2
|
+
|
3
|
+
class AbstractClientTest < Test::Unit::TestCase
|
4
|
+
|
5
|
+
context 'testing out a connection' do
|
6
|
+
|
7
|
+
setup do
|
8
|
+
@client = Marvin::Settings.client
|
9
|
+
@client.setup
|
10
|
+
@config = @client.configuration
|
11
|
+
@client.configuration = {
|
12
|
+
:user => "DemoUser",
|
13
|
+
:name => "Demo Users Name",
|
14
|
+
:nick => "Haysoos",
|
15
|
+
:nicks => ["Haysoos_", "Haysoos__"]
|
16
|
+
}
|
17
|
+
end
|
18
|
+
|
19
|
+
should "dispatch :client_connected as the first event on process_connect" do
|
20
|
+
assert_resets_client
|
21
|
+
client.process_connect
|
22
|
+
assert_equal [:client_connected, {}], client.dispatched_events.first
|
23
|
+
assert_dispatched :client_connected, 0, {}
|
24
|
+
end
|
25
|
+
|
26
|
+
should "dispatch :client_connected as the first event on process_connect" do
|
27
|
+
assert_resets_client
|
28
|
+
client.default_channels = ["#awesome", "#rock"]
|
29
|
+
client.process_connect
|
30
|
+
assert_dispatched :client_connected, -2, {}
|
31
|
+
assert_dispatched :outgoing_nick, -1
|
32
|
+
assert_equal 2, client.outgoing_commands.length
|
33
|
+
assert_equal "NICK Haysoos\r\n", client.outgoing_commands[0]
|
34
|
+
assert_sent_line "NICK Haysoos\r\n", 0
|
35
|
+
assert_sent_line "USER DemoUser 0 \* :Demo Users Name\r\n", 1
|
36
|
+
end
|
37
|
+
|
38
|
+
should "dispatch :client_disconnect on process_disconnect" do
|
39
|
+
assert_resets_client
|
40
|
+
client.process_disconnect
|
41
|
+
assert_dispatched :client_disconnected
|
42
|
+
end
|
43
|
+
|
44
|
+
should 'attempt to join the default channels on receiving welcome' do
|
45
|
+
assert_resets_client
|
46
|
+
client.default_channels = ["#awesome", "#rock"]
|
47
|
+
client.handle_welcome
|
48
|
+
assert_sent_line "JOIN #awesome,#rock\r\n"
|
49
|
+
end
|
50
|
+
|
51
|
+
should "add an :incoming_line event for each incoming line" do
|
52
|
+
assert_resets_client
|
53
|
+
client.receive_line "SOME RANDOM LINE THAT HAS ZERO ACTUAL USE"
|
54
|
+
assert_dispatched :incoming_line, 0, :line => "SOME RANDOM LINE THAT HAS ZERO ACTUAL USE"
|
55
|
+
end
|
56
|
+
|
57
|
+
teardown do
|
58
|
+
@client.configuration = @config
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|