marvin 0.8.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. data/bin/marvin +33 -0
  2. data/handlers/debug_handler.rb +5 -0
  3. data/handlers/hello_world.rb +9 -0
  4. data/handlers/keiki_thwopper.rb +21 -0
  5. data/handlers/simple_logger.rb +24 -0
  6. data/handlers/tweet_tweet.rb +19 -0
  7. data/lib/marvin.rb +56 -0
  8. data/lib/marvin/abstract_client.rb +146 -0
  9. data/lib/marvin/abstract_parser.rb +29 -0
  10. data/lib/marvin/base.rb +195 -0
  11. data/lib/marvin/client/actions.rb +104 -0
  12. data/lib/marvin/client/default_handlers.rb +97 -0
  13. data/lib/marvin/command_handler.rb +91 -0
  14. data/lib/marvin/console.rb +50 -0
  15. data/lib/marvin/core_commands.rb +49 -0
  16. data/lib/marvin/distributed.rb +8 -0
  17. data/lib/marvin/distributed/client.rb +225 -0
  18. data/lib/marvin/distributed/handler.rb +85 -0
  19. data/lib/marvin/distributed/protocol.rb +88 -0
  20. data/lib/marvin/distributed/server.rb +154 -0
  21. data/lib/marvin/dsl.rb +103 -0
  22. data/lib/marvin/exception_tracker.rb +19 -0
  23. data/lib/marvin/exceptions.rb +11 -0
  24. data/lib/marvin/irc.rb +7 -0
  25. data/lib/marvin/irc/client.rb +168 -0
  26. data/lib/marvin/irc/event.rb +39 -0
  27. data/lib/marvin/irc/replies.rb +154 -0
  28. data/lib/marvin/logging_handler.rb +76 -0
  29. data/lib/marvin/middle_man.rb +103 -0
  30. data/lib/marvin/parsers.rb +9 -0
  31. data/lib/marvin/parsers/command.rb +107 -0
  32. data/lib/marvin/parsers/prefixes.rb +8 -0
  33. data/lib/marvin/parsers/prefixes/host_mask.rb +35 -0
  34. data/lib/marvin/parsers/prefixes/server.rb +24 -0
  35. data/lib/marvin/parsers/ragel_parser.rb +720 -0
  36. data/lib/marvin/parsers/ragel_parser.rl +143 -0
  37. data/lib/marvin/parsers/simple_parser.rb +35 -0
  38. data/lib/marvin/settings.rb +31 -0
  39. data/lib/marvin/test_client.rb +58 -0
  40. data/lib/marvin/util.rb +54 -0
  41. data/templates/boot.erb +3 -0
  42. data/templates/connections.yml.erb +10 -0
  43. data/templates/debug_handler.erb +5 -0
  44. data/templates/hello_world.erb +10 -0
  45. data/templates/rakefile.erb +15 -0
  46. data/templates/settings.yml.erb +8 -0
  47. data/templates/setup.erb +31 -0
  48. data/templates/test_helper.erb +17 -0
  49. data/test/abstract_client_test.rb +63 -0
  50. data/test/parser_comparison.rb +62 -0
  51. data/test/parser_test.rb +266 -0
  52. data/test/test_helper.rb +62 -0
  53. data/test/util_test.rb +57 -0
  54. 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
@@ -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
@@ -0,0 +1,3 @@
1
+ require 'rubygems'
2
+ require 'marvin'
3
+ Marvin::Settings.root = Pathname.new(__FILE__).dirname.join("..").expand_path
@@ -0,0 +1,10 @@
1
+ "irc.freenode.net":
2
+ port: 6667
3
+ channels:
4
+ - "#marvin-testing"
5
+
6
+ "irc.oftc.net":
7
+ port: 6697
8
+ ssl: true
9
+ channels:
10
+ - "#marvin-testing"
@@ -0,0 +1,5 @@
1
+ # Use this class to debug stuff as you
2
+ # go along - e.g. dump events etc.
3
+ class DebugHandler < Marvin::CommandHandler
4
+
5
+ end
@@ -0,0 +1,10 @@
1
+ class HelloWorld < Marvin::CommandHandler
2
+
3
+ exposes :hello
4
+
5
+ desc "Says hello to you (with the current pid!)"
6
+ def hello(data)
7
+ reply "Hola from process with pid #{Process.pid}!"
8
+ end
9
+
10
+ end
@@ -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
@@ -0,0 +1,8 @@
1
+ default:
2
+ user: MarvinBot
3
+ name: MarvinBot
4
+ nick: MarvinBot
5
+ nicks:
6
+ - MarvinBot1000
7
+ - MarvinBot2000
8
+ - MarvinBot3000
@@ -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