jeffrafter-marvin 0.1.20081115 → 0.1.20081120

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. data/README.textile +105 -32
  2. data/VERSION.yml +1 -1
  3. data/bin/marvin +10 -6
  4. data/config/connections.yml.sample +5 -0
  5. data/config/settings.yml.sample +2 -7
  6. data/config/setup.rb +6 -1
  7. data/handlers/debug_handler.rb +5 -0
  8. data/handlers/hello_world.rb +1 -1
  9. data/lib/marvin.rb +13 -9
  10. data/lib/marvin/abstract_client.rb +88 -43
  11. data/lib/marvin/abstract_parser.rb +14 -2
  12. data/lib/marvin/base.rb +44 -6
  13. data/lib/marvin/dispatchable.rb +9 -4
  14. data/lib/marvin/exception_tracker.rb +1 -1
  15. data/lib/marvin/exceptions.rb +3 -0
  16. data/lib/marvin/irc.rb +4 -5
  17. data/lib/marvin/irc/client.rb +39 -7
  18. data/lib/marvin/irc/event.rb +9 -4
  19. data/lib/marvin/irc/replies.rb +154 -0
  20. data/lib/marvin/loader.rb +26 -8
  21. data/lib/marvin/logger.rb +66 -3
  22. data/lib/marvin/options.rb +33 -0
  23. data/lib/marvin/parsers.rb +3 -0
  24. data/lib/marvin/parsers/command.rb +105 -0
  25. data/lib/marvin/parsers/prefixes.rb +8 -0
  26. data/lib/marvin/parsers/prefixes/host_mask.rb +30 -0
  27. data/lib/marvin/parsers/prefixes/server.rb +24 -0
  28. data/lib/marvin/parsers/ragel_parser.rb +713 -0
  29. data/lib/marvin/parsers/ragel_parser.rl +144 -0
  30. data/lib/marvin/parsers/regexp_parser.rb +0 -3
  31. data/lib/marvin/parsers/simple_parser.rb +20 -81
  32. data/lib/marvin/settings.rb +9 -9
  33. data/lib/marvin/test_client.rb +5 -1
  34. data/lib/marvin/util.rb +20 -3
  35. data/script/{run → client} +0 -0
  36. data/script/daemon-runner +1 -1
  37. data/script/install +3 -0
  38. data/test/parser_comparison.rb +62 -0
  39. data/test/parser_test.rb +264 -0
  40. data/test/test_helper.rb +10 -0
  41. metadata +19 -9
  42. data/lib/marvin/drb_handler.rb +0 -7
  43. data/lib/marvin/irc/abstract_server.rb +0 -4
  44. data/lib/marvin/irc/base_server.rb +0 -11
  45. data/lib/marvin/irc/socket_client.rb +0 -69
  46. data/lib/marvin/parsers/simple_parser/default_events.rb +0 -37
  47. data/lib/marvin/parsers/simple_parser/event_extensions.rb +0 -14
  48. data/lib/marvin/parsers/simple_parser/prefixes.rb +0 -34
@@ -7,30 +7,85 @@ particular need.
7
7
 
8
8
  h2. Background
9
9
 
10
- Marvin is an event driven framework in two ways - for one, it uses
11
- EventMachine for all networking purposes - as a result, it's both
12
- relatively stable / reliable and also powerful.
13
-
14
- Following on from this, the irc library is event driven. At the base
15
- level, you choose a client (By Default, Marvin::IRC::Client.) and then you register
16
- any number of handlers. Whenever an event happens e.g. an incoming message,
17
- a connection unbinding or event just post_init, each handler is notified
18
- and given a small set of details about the event.
19
-
20
- Handlers are very simple - in fact, you could get away with registering
21
- Object.new as a handler.
22
-
23
- To function, handlers only require one method: handle - which takes
24
- two options. an event name (e.g. :incoming_message) and a hash
25
- of the aforementioned attributes / details. This data can then be processed.
26
- Alternatively, if a handler has a "handle_[event_name]" method (e.g.
27
- handle_incoming_message), it will instead be called. Also, if client=
28
- is implemented this will be called when the client is setup containing
29
- a reference to said client. This is used to that the handler can
30
- respond to actions.
31
-
32
- Like Rack for HTTP, Marvin provides a fair amount of example
33
- handlers for simple stuff inside IRC.
10
+ The library is designed to be event driven in that it:
11
+
12
+ # Uses the EventMachine library for all network connections
13
+ # It uses an architecture based on event listeners - called 'handlers'
14
+
15
+ It's been heavily influenced by rack in terms of design, making it easy
16
+ to do things like chain handlers, write your own functionality and most
17
+ of all making it easy to implement.
18
+
19
+ h2. Getting Started
20
+
21
+ The easiest way to get started with Marvin is by installing the Marvin gem. To
22
+ do this, make sure Github is added to your gem sources (and you are using
23
+ rubygems >= 1.2.0) (by default, substitute username for Sutto):
24
+
25
+ $ gem sources -a http://gems.github.com
26
+ $ sudo gem install username-marvin
27
+
28
+
29
+ Once you have installed the gem, you should have access to the "marvin" command:
30
+
31
+ $ marvin --help
32
+
33
+ You can create a new marvin folder:
34
+
35
+ $ marvin create my_marvin_project
36
+
37
+ Then simply edit your settings in the +config/settings.yml+
38
+
39
+ default:
40
+ name: Marvin
41
+ use_logging: false
42
+ datastore_location: tmp/datastore.json
43
+ development:
44
+ user: MarvinBot
45
+ name: MarvinBot
46
+ nick: Marvin
47
+
48
+ You can use the defaults or configure it. The datastore location
49
+ specifies a relative path where a simple json-backed key value
50
+ store will store persistent information for your client (if chosen).
51
+ Once that's been done, you'll want to setup some connections by editing
52
+ +config/connections.yml+, using the following format:
53
+
54
+ "server-address":
55
+ post: 6667 # Defaults to 6667
56
+ channels:
57
+ - "#marvin-testing"
58
+ - "#relayrelay"
59
+ nicks:
60
+ - List
61
+ - Of
62
+ - Alternative
63
+ - Nicks
64
+ "another-server-address":
65
+ post: 6667 # Defaults to 6667
66
+ channels:
67
+ - "#helloworld"
68
+
69
+ Which will let marvin connect to multiple servers - autojoining the specific rooms.
70
+ Next, to get started you can simply type:
71
+
72
+ $ ./script/client
73
+
74
+ The bot should join the specified channel and will respond to some simple
75
+ commands by default:
76
+
77
+ *YourName*: MarvinBot3000: hello
78
+ *MarvinBot3000*: YourName: Hola!
79
+
80
+ As defined in handlers/hello_world.rb
81
+
82
+ h2. Thanks
83
+
84
+ Thanks goes out to the following people / projects:
85
+
86
+ * Jeff Rafter - contributed code and doc changes, now one of the co-developers.
87
+ * epitron / halogrium - For the ragel state machine used in Marvin::Parsers::RagelParser
88
+ * The creator of Ruby-IRCD - the server component is heavily influenced by / part derivative of said work.
34
89
 
35
90
  h2. Marvin::Base - A handler starting point
36
91
 
@@ -53,13 +108,15 @@ openstruct version of the details. e.g.
53
108
  Or the like. Also, the halt! method can be called in any subclass to
54
109
  halt the handler callback chain.
55
110
 
111
+ You also get access to the class method +on_numeric+ which makes
112
+ it relatively easy to respond to a specific numeric reply.
113
+
56
114
  h2. Marvin::CommandHandler - Ridiculously easy Bots
57
115
 
58
116
  With Marvin::CommandHandler, you get to define seriously
59
117
  simple classes which can act as a simple bot. It takes
60
118
  great inspiration from "MatzBot":http://github.com/defunkt/matzbot/tree/master
61
- which was actually one of the main inspirations for
62
- creating marvin.
119
+ to make it as easy as possible to make a simple bot
63
120
 
64
121
  To write a CommandHandler, you simply create a subclass
65
122
  (ala ActiveRecord::Base), define a few methods and then
@@ -75,7 +132,15 @@ just use the "exposes" class method. e.g.
75
132
  Where data is an array of parameters. exposed methods will be called
76
133
  when they match the following pattern:
77
134
 
78
- Botname: <exposed-method> <space-seperated-list-meaning-data>
135
+ Botname: *exposed-method* *space-seperated-list-meaning-data*
136
+
137
+ i.e., the above handler could be called in IRC as such:
138
+
139
+ YourBotsName: hello
140
+
141
+ or, even easier, by PM'ing the bot with:
142
+
143
+ hello
79
144
 
80
145
  h2. Marvin::MiddleMan - Introducing middleware
81
146
 
@@ -83,9 +148,10 @@ Marvin::MiddleMan lets you insert middleware between handlers
83
148
  and you're client - letting you do things such as translating
84
149
  all messages on the fly. It's build to be extensible and is
85
150
  relatively simple to use. On any Marvin::Base subclass (baring
86
- the MiddleMan itself), you can simple use the normal methods
87
- of registering a handler with one exception - you now pass
88
- one argument, the class reference to your middleman class.
151
+ the MiddleMan itself), using a middle man is easy - you simply
152
+ call the register! class method with an option argument. e.g:
153
+
154
+ HelloWorld.register! Marvin::MiddleMan
89
155
 
90
156
  h2. Marvin::DataStore - A dead simple persistent hash store
91
157
 
@@ -104,7 +170,14 @@ If you're inside a Marvin::Base subclass it's even easier. You can get a cattr_a
104
170
  style accessor for free - just use the "uses_datastore" method. e.g:
105
171
 
106
172
  class X < Marvin::Base
107
- uses_datastore "datastore-global-key", :cattr_name
173
+ uses_datastore "datastore-global-key", :something
174
+ end
175
+
176
+ Then, self.something will point to the data store - letting you do
177
+ things like:
178
+
179
+ def hello(data)
180
+ (self.something[from] ||= 0) += 1
108
181
  end
109
182
 
110
- Then, self.cattr_name will point to the data store instance.
183
+ which will persist the count between each session.
@@ -1,4 +1,4 @@
1
1
  ---
2
- patch: 20081115
2
+ patch: 20081120
3
3
  major: 0
4
4
  minor: 1
data/bin/marvin CHANGED
@@ -39,17 +39,21 @@ if ARGV.length >= 1 && !["start", "stop", "run", "restart"].include?(ARGV[0])
39
39
  puts "Writing Settings file"
40
40
  copy "config/settings.yml.sample", "config/settings.yml"
41
41
 
42
+ puts "Writing Connections file"
43
+ copy "config/connections.yml.sample", "config/connections.yml"
44
+
42
45
  puts "Writing setup.rb"
43
46
  copy "config/setup.rb"
44
47
 
45
- puts "Copying start script - script/run"
46
- copy "script/run"
48
+ puts "Copying start scripts"
49
+ copy "script/client"
47
50
  copy "script/daemon-runner"
48
- FileUtils.chmod 0755, j(DEST, "script/run")
51
+ FileUtils.chmod 0755, j(DEST, "script/client")
49
52
  FileUtils.chmod 0755, j(DEST, "script/daemon-runner")
50
53
 
51
- puts "Copying example handler"
54
+ puts "Copying example handlers"
52
55
  copy "handlers/hello_world.rb"
56
+ copy "handlers/debug_handler.rb"
53
57
 
54
58
  puts "Done!"
55
59
  elsif ARGV.length >= 1
@@ -59,9 +63,9 @@ elsif ARGV.length >= 1
59
63
  end
60
64
  exec "script/daemon-runner #{ARGV.map {|a| a.include?(" ") ? "\"#{a}\"" : a }.join(" ")}"
61
65
  else
62
- if !File.exist?("script/run")
66
+ if !File.exist?("script/client")
63
67
  puts "Woops! This isn't a marvin directory."
64
68
  exit(1)
65
69
  end
66
- exec "script/run"
70
+ exec "script/client"
67
71
  end
@@ -0,0 +1,5 @@
1
+ "irc.freenode.net":
2
+ port: 6667
3
+ channels:
4
+ - "#marvin-testing"
5
+ - "#relayrelay"
@@ -1,13 +1,8 @@
1
1
  default:
2
- name: "My Marvin Bot"
3
- server: irc.freenode.net
4
- port: 6667
5
- channel: "#marvin-testing"
2
+ name: Marvin
6
3
  use_logging: false
7
4
  datastore_location: tmp/datastore.json
8
5
  development:
9
6
  user: MarvinBot
10
7
  name: MarvinBot
11
- nick: MarvinBot3000
12
- production:
13
- deployed: false
8
+ nick: Marvin
@@ -9,6 +9,11 @@ Marvin::Loader.before_connecting do
9
9
  # Example Handler use.
10
10
  # LoggingHandler.register! if Marvin::Settings.use_logging
11
11
 
12
- HelloWorld.register!
12
+ if Marvin::Loader.type == :client
13
+ Marvin::Distributed::DispatchHandler.register!
14
+ else
15
+ HelloWorld.register!
16
+ DebugHandler.register!
17
+ end
13
18
 
14
19
  end
@@ -0,0 +1,5 @@
1
+ # Used for debugging stuff - trying
2
+ # adding misc stuff to play around with it
3
+ class DebugHandler < Marvin::Base
4
+
5
+ end
@@ -3,7 +3,7 @@ class HelloWorld < Marvin::CommandHandler
3
3
  exposes :hello
4
4
 
5
5
  def hello(data)
6
- reply "Hola!" unless target == "#all"
6
+ reply "Hola from process with pid #{Process.pid}!"
7
7
  end
8
8
 
9
9
  end
@@ -8,8 +8,17 @@ require 'marvin/core_ext'
8
8
  require 'marvin/exceptions'
9
9
 
10
10
  module Marvin
11
+ module VERSION
12
+ MAJOR = 0
13
+ MINOR = 1
14
+ PATCH = 20081120
15
+
16
+ STRING = [MAJOR, MINOR, PATCH].join(".")
17
+ end
18
+
11
19
  autoload :Util, 'marvin/util'
12
20
  autoload :Dispatchable, 'marvin/dispatchable'
21
+ autoload :Distributed, 'marvin/distributed'
13
22
  autoload :AbstractClient, 'marvin/abstract_client'
14
23
  autoload :Base, 'marvin/base'
15
24
  autoload :ClientMixin, 'marvin/client_mixin'
@@ -22,6 +31,7 @@ module Marvin
22
31
  autoload :DRBHandler, 'marvin/drb_handler'
23
32
  autoload :DataStore, 'marvin/data_store'
24
33
  autoload :ExceptionTracker, 'marvin/exception_tracker'
34
+ autoload :Options, 'marvin/options'
25
35
  # Parsers
26
36
  autoload :AbstractParser, 'marvin/abstract_parser'
27
37
  autoload :Parsers, 'marvin/parsers.rb'
@@ -31,14 +41,8 @@ module Marvin
31
41
 
32
42
  Settings.setup # Load Settings etc.
33
43
 
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}"
44
+ def self.version
45
+ VERSION::STRING
43
46
  end
47
+
44
48
  end
@@ -7,8 +7,17 @@ module Marvin
7
7
 
8
8
  include Marvin::Dispatchable
9
9
 
10
+ def initialize(opts = {})
11
+ self.original_opts = opts.dup # Copy the options so we can use them to reconnect.
12
+ self.server = opts[:server]
13
+ self.port = opts[:port]
14
+ self.default_channels = opts[:channels]
15
+ self.nicks = opts[:nicks] || []
16
+ self.pass = opts[:pass]
17
+ end
18
+
10
19
  cattr_accessor :events, :configuration, :logger, :is_setup, :connections
11
- attr_accessor :channels, :nickname
20
+ attr_accessor :channels, :nickname, :server, :port, :nicks, :pass, :disconnect_expected, :original_opts
12
21
 
13
22
  # Set the default values for the variables
14
23
  self.events = []
@@ -22,19 +31,25 @@ module Marvin
22
31
  # call #client= on each handler if they respond to it.
23
32
  def process_connect
24
33
  self.class.setup
25
- logger.debug "Initializing the current instance"
34
+ logger.info "Initializing the current instance"
26
35
  self.channels = []
27
36
  self.connections << self
28
- logger.debug "Setting the client for each handler"
37
+ logger.info "Setting the client for each handler"
29
38
  self.handlers.each { |h| h.client = self if h.respond_to?(:client=) }
30
- logger.debug "Dispatching the default :client_connected event"
39
+ logger.info "Dispatching the default :client_connected event"
31
40
  dispatch :client_connected
32
41
  end
33
42
 
34
43
  def process_disconnect
44
+ logger.info "Handling disconnect for #{self.server}:#{self.port}"
35
45
  self.connections.delete(self) if self.connections.include?(self)
36
46
  dispatch :client_disconnected
37
- Marvin::Loader.stop! if self.connections.blank?
47
+ unless self.disconnect_expected
48
+ logger.warn "Lost connection to server - adding reconnect"
49
+ self.class.add_reconnect self.original_opts
50
+ else
51
+ Marvin::Loader.stop! if self.connections.blank?
52
+ end
38
53
  end
39
54
 
40
55
  # Sets the current class-wide settings of this IRC Client
@@ -51,11 +66,6 @@ module Marvin
51
66
  # that is more widely used throughout the client.
52
67
  def self.setup
53
68
  return if self.is_setup
54
- # Default the logger back to a new one.
55
- self.configuration.channels ||= []
56
- unless self.configuration.channel.blank? || self.configuration.channels.include?(self.configuration.channel)
57
- self.configuration.channels.unshift(self.configuration.channel)
58
- end
59
69
  if configuration.logger.blank?
60
70
  require 'logger'
61
71
  configuration.logger = Marvin::Logger.logger
@@ -80,40 +90,39 @@ module Marvin
80
90
  # to be in and if a password is specified in the configuration,
81
91
  # it will also attempt to identify us.
82
92
  def handle_client_connected(opts = {})
83
- logger.debug "About to handle post init"
93
+ logger.info "About to handle client connected"
94
+ # If the pass is set
95
+ unless self.pass.blank?
96
+ logger.info "Sending pass for connection"
97
+ command :pass, self.pass
98
+ end
84
99
  # IRC Connection is establish so we send all the required commands to the server.
85
- logger.debug "Setting default nickname"
86
- default_nickname = self.configuration.nick || self.configuration.nicknames.shift
100
+ logger.info "Setting default nickname"
101
+ default_nickname = self.nicks.shift
87
102
  nick default_nickname
88
- logger.debug "sending user command"
103
+ logger.info "sending user command"
89
104
  command :user, self.configuration.user, "0", "*", Marvin::Util.last_param(self.configuration.name)
90
- # If a password is specified, we will attempt to message
91
- # NickServ to identify ourselves.
92
- say ":IDENTIFY #{self.configuration.password}", "NickServ" unless self.configuration.password.blank?
93
- # Join the default channels
94
- self.configuration.channels.each { |c| self.join c }
95
105
  rescue Exception => e
96
106
  Marvin::ExceptionTracker.log(e)
97
107
  end
98
-
99
- # The default handler for when a users nickname is taken on
100
- # on the server. It will attempt to get the nicknickname from
101
- # the nicknames part of the configuration (if available) and
102
- # will then call #nick to change the nickname.
103
- def handle_incoming_nick_taken(opts = {})
104
- logger.info "Nick Is Taken"
105
- logger.debug "Available Nicknames: #{self.configuration.nicknames.to_a.join(", ")}"
106
- available_nicknames = self.configuration.nicknames.to_a
107
- if available_nicknames.length > 0
108
- logger.debug "Getting next nickname to switch"
109
- next_nick = available_nicknames.shift # Get the next nickname
110
- self.configuration.nicknames = available_nicknames
111
- logger.info "Attemping to set nickname to #{new_nick}"
112
- nick next_nick
113
- else
114
- logger.info "No Nicknames available - QUITTING"
115
- quit
108
+
109
+ def default_channels
110
+ @default_channels ||= []
111
+ end
112
+
113
+ def default_channels=(channels)
114
+ @default_channels = channels.to_a.map { |c| c.to_s }
115
+ end
116
+
117
+ def nicks
118
+ if @nicks.blank? && !@nicks_loaded
119
+ logger.info "Setting default nick list"
120
+ @nicks = []
121
+ @nicks << self.configuration.nick
122
+ @nicks += self.configuration.nicks.to_a unless self.configuration.nicks.blank?
123
+ @nicks_loaded
116
124
  end
125
+ return @nicks
117
126
  end
118
127
 
119
128
  # The default response for PING's - it simply replies
@@ -126,9 +135,43 @@ module Marvin
126
135
  # TODO: Get the correct mapping for a given
127
136
  # Code.
128
137
  def handle_incoming_numeric(opts = {})
138
+ case opts[:code]
139
+ when Marvin::IRC::Replies[:RPL_WELCOME]
140
+ handle_welcome
141
+ when Marvin::IRC::Replies[:ERR_NICKNAMEINUSE]
142
+ handle_nick_taken
143
+ end
129
144
  code = opts[:code].to_i
130
145
  args = Marvin::Util.arguments(opts[:data])
131
- dispatch :incoming_numeric_processed, {:code => code, :data => args}
146
+ dispatch :incoming_numeric_processed, :code => code, :data => args
147
+ end
148
+
149
+ def handle_welcome
150
+ logger.info "Say hello to my little friend - Got welcome"
151
+ # If a password is specified, we will attempt to message
152
+ # NickServ to identify ourselves.
153
+ say ":IDENTIFY #{self.configuration.password}", "NickServ" unless self.configuration.password.blank?
154
+ # Join the default channels IF they're already set
155
+ # Note that Marvin::IRC::Client.connect will set them AFTER this stuff is run.
156
+ self.default_channels.each { |c| self.join(c) }
157
+ end
158
+
159
+ # The default handler for when a users nickname is taken on
160
+ # on the server. It will attempt to get the nicknickname from
161
+ # the nicknames part of the configuration (if available) and
162
+ # will then call #nick to change the nickname.
163
+ def handle_nick_taken
164
+ logger.info "Nickname '#{self.nickname}' on #{self.server} taken, trying next."
165
+ logger.info "Available Nicknames: #{self.nicks.empty? ? "None" : self.nicks.join(", ")}"
166
+ if !self.nicks.empty?
167
+ logger.info "Getting next nickname to switch"
168
+ next_nick = self.nicks.shift # Get the next nickname
169
+ logger.info "Attemping to set nickname to '#{next_nick}'"
170
+ nick next_nick
171
+ else
172
+ logger.fatal "No Nicknames available - QUITTING"
173
+ quit
174
+ end
132
175
  end
133
176
 
134
177
  ## General IRC Functions
@@ -141,13 +184,14 @@ module Marvin
141
184
  # First, get the appropriate command
142
185
  name = name.to_s.upcase
143
186
  args = args.flatten.compact
144
- irc_command = "#{name} #{args.join(" ").strip} \r\n"
187
+ irc_command = "#{name} #{args.join(" ").strip}\r\n"
145
188
  send_line irc_command
146
189
  end
147
190
 
148
191
  def join(channel)
149
192
  channel = Marvin::Util.channel_name(channel)
150
193
  # Record the fact we're entering the room.
194
+ # TODO: Refactor to only add the channel when we receive confirmation we've joined.
151
195
  self.channels << channel
152
196
  command :JOIN, channel
153
197
  logger.info "Joined channel #{channel}"
@@ -166,13 +210,14 @@ module Marvin
166
210
  end
167
211
 
168
212
  def quit(reason = nil)
169
- logger.debug "Preparing to part from #{self.channels.size} channels"
213
+ self.disconnect_expected = true
214
+ logger.info "Preparing to part from #{self.channels.size} channels"
170
215
  self.channels.to_a.each do |chan|
171
- logger.debug "Parting from #{chan}"
216
+ logger.info "Parting from #{chan}"
172
217
  self.part chan, reason
173
218
  end
174
- logger.debug "Parted from all channels, quitting"
175
- command :quit
219
+ logger.info "Parted from all channels, quitting"
220
+ command :quit
176
221
  dispatch :quit
177
222
  # Remove the connections from the pool
178
223
  self.connections.delete(self)