marvin 0.8.0.0
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/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
|