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.
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