Sutto-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 (44) hide show
  1. data/README.textile +70 -50
  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 +1 -0
  7. data/handlers/debug_handler.rb +5 -0
  8. data/handlers/hello_world.rb +1 -1
  9. data/lib/marvin.rb +1 -0
  10. data/lib/marvin/abstract_client.rb +79 -42
  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/drb_handler.rb +7 -2
  15. data/lib/marvin/exceptions.rb +3 -0
  16. data/lib/marvin/irc.rb +1 -1
  17. data/lib/marvin/irc/client.rb +31 -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 +1 -0
  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 +73 -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 +8 -8
  33. data/lib/marvin/util.rb +2 -2
  34. data/script/{run → client} +0 -0
  35. data/script/daemon-runner +1 -1
  36. data/script/install +3 -0
  37. data/test/parser_comparison.rb +36 -0
  38. data/test/parser_test.rb +20 -0
  39. data/test/test_helper.rb +10 -0
  40. metadata +19 -9
  41. data/lib/marvin/irc/socket_client.rb +0 -69
  42. data/lib/marvin/parsers/simple_parser/default_events.rb +0 -37
  43. data/lib/marvin/parsers/simple_parser/event_extensions.rb +0 -14
  44. data/lib/marvin/parsers/simple_parser/prefixes.rb +0 -34
data/README.textile CHANGED
@@ -7,39 +7,24 @@ 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.
34
18
 
35
19
  h2. Getting Started
36
20
 
37
21
  The easiest way to get started with Marvin is by installing the Marvin gem. To
38
22
  do this, make sure Github is added to your gem sources (and you are using
39
- rubygems >= 1.2.0):
23
+ rubygems >= 1.2.0) (by default, substitute username for Sutto):
40
24
 
41
25
  $ gem sources -a http://gems.github.com
42
26
  $ sudo gem install username-marvin
27
+
43
28
 
44
29
  Once you have installed the gem, you should have access to the "marvin" command:
45
30
 
@@ -52,30 +37,47 @@ You can create a new marvin folder:
52
37
  Then simply edit your settings in the +config/settings.yml+
53
38
 
54
39
  default:
55
- name: "My Marvin Bot"
56
- server: irc.freenode.net
57
- port: 6667
58
- channel: "#marvin-testing"
40
+ name: Marvin
59
41
  use_logging: false
60
42
  datastore_location: tmp/datastore.json
61
43
  development:
62
44
  user: MarvinBot
63
45
  name: MarvinBot
64
- nick: MarvinBot3000
65
- production:
66
- deployed: false
67
-
68
- You can use the defaults or configure it. By default your marvin bot will not
69
- do any logging. You will need to set that up. From this point you can begin
70
- running the bot as a daemon or as a console application:
71
-
72
- $ ./script/run
73
-
74
- The bot should join the specified channel and will resond to some simple
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
75
  commands by default:
76
76
 
77
- |<YourName> MarvinBot3000: hello
78
- |<MarvinBot3000> YourName: Hola!
77
+ *YourName*: MarvinBot3000: hello
78
+ *MarvinBot3000*: YourName: Hola!
79
+
80
+ As defined in handlers/hello_world.rb
79
81
 
80
82
  h2. Marvin::Base - A handler starting point
81
83
 
@@ -98,13 +100,15 @@ openstruct version of the details. e.g.
98
100
  Or the like. Also, the halt! method can be called in any subclass to
99
101
  halt the handler callback chain.
100
102
 
103
+ You also get access to the class method +on_numeric+ which makes
104
+ it relatively easy to respond to a specific numeric reply.
105
+
101
106
  h2. Marvin::CommandHandler - Ridiculously easy Bots
102
107
 
103
108
  With Marvin::CommandHandler, you get to define seriously
104
109
  simple classes which can act as a simple bot. It takes
105
110
  great inspiration from "MatzBot":http://github.com/defunkt/matzbot/tree/master
106
- which was actually one of the main inspirations for
107
- creating marvin.
111
+ to make it as easy as possible to make a simple bot
108
112
 
109
113
  To write a CommandHandler, you simply create a subclass
110
114
  (ala ActiveRecord::Base), define a few methods and then
@@ -120,7 +124,15 @@ just use the "exposes" class method. e.g.
120
124
  Where data is an array of parameters. exposed methods will be called
121
125
  when they match the following pattern:
122
126
 
123
- Botname: <exposed-method> <space-seperated-list-meaning-data>
127
+ Botname: *exposed-method* *space-seperated-list-meaning-data*
128
+
129
+ i.e., the above handler could be called in IRC as such:
130
+
131
+ YourBotsName: hello
132
+
133
+ or, even easier, by PM'ing the bot with:
134
+
135
+ hello
124
136
 
125
137
  h2. Marvin::MiddleMan - Introducing middleware
126
138
 
@@ -128,9 +140,10 @@ Marvin::MiddleMan lets you insert middleware between handlers
128
140
  and you're client - letting you do things such as translating
129
141
  all messages on the fly. It's build to be extensible and is
130
142
  relatively simple to use. On any Marvin::Base subclass (baring
131
- the MiddleMan itself), you can simple use the normal methods
132
- of registering a handler with one exception - you now pass
133
- one argument, the class reference to your middleman class.
143
+ the MiddleMan itself), using a middle man is easy - you simply
144
+ call the register! class method with an option argument. e.g:
145
+
146
+ HelloWorld.register! Marvin::MiddleMan
134
147
 
135
148
  h2. Marvin::DataStore - A dead simple persistent hash store
136
149
 
@@ -149,7 +162,14 @@ If you're inside a Marvin::Base subclass it's even easier. You can get a cattr_a
149
162
  style accessor for free - just use the "uses_datastore" method. e.g:
150
163
 
151
164
  class X < Marvin::Base
152
- uses_datastore "datastore-global-key", :cattr_name
165
+ uses_datastore "datastore-global-key", :something
166
+ end
167
+
168
+ Then, self.something will point to the data store - letting you do
169
+ things like:
170
+
171
+ def hello(data)
172
+ (self.something[from] ||= 0) += 1
153
173
  end
154
174
 
155
- Then, self.cattr_name will point to the data store instance.
175
+ which will persist the count between each session.
data/VERSION.yml CHANGED
@@ -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
data/config/setup.rb CHANGED
@@ -10,5 +10,6 @@ Marvin::Loader.before_connecting do
10
10
  # LoggingHandler.register! if Marvin::Settings.use_logging
11
11
 
12
12
  HelloWorld.register!
13
+ DebugHandler.register!
13
14
 
14
15
  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!"
7
7
  end
8
8
 
9
9
  end
data/lib/marvin.rb CHANGED
@@ -22,6 +22,7 @@ module Marvin
22
22
  autoload :DRBHandler, 'marvin/drb_handler'
23
23
  autoload :DataStore, 'marvin/data_store'
24
24
  autoload :ExceptionTracker, 'marvin/exception_tracker'
25
+ autoload :Options, 'marvin/options'
25
26
  # Parsers
26
27
  autoload :AbstractParser, 'marvin/abstract_parser'
27
28
  autoload :Parsers, 'marvin/parsers.rb'
@@ -7,8 +7,16 @@ module Marvin
7
7
 
8
8
  include Marvin::Dispatchable
9
9
 
10
+ def initialize(opts = {})
11
+ self.server = opts[:server]
12
+ self.port = opts[:port]
13
+ self.default_channels = opts[:channels]
14
+ self.nicks = opts[:nicks] || []
15
+ self.pass = opts[:pass]
16
+ end
17
+
10
18
  cattr_accessor :events, :configuration, :logger, :is_setup, :connections
11
- attr_accessor :channels, :nickname
19
+ attr_accessor :channels, :nickname, :server, :port, :nicks, :pass
12
20
 
13
21
  # Set the default values for the variables
14
22
  self.events = []
@@ -22,16 +30,17 @@ module Marvin
22
30
  # call #client= on each handler if they respond to it.
23
31
  def process_connect
24
32
  self.class.setup
25
- logger.debug "Initializing the current instance"
33
+ logger.info "Initializing the current instance"
26
34
  self.channels = []
27
35
  self.connections << self
28
- logger.debug "Setting the client for each handler"
36
+ logger.info "Setting the client for each handler"
29
37
  self.handlers.each { |h| h.client = self if h.respond_to?(:client=) }
30
- logger.debug "Dispatching the default :client_connected event"
38
+ logger.info "Dispatching the default :client_connected event"
31
39
  dispatch :client_connected
32
40
  end
33
41
 
34
42
  def process_disconnect
43
+ logger.info "Handling disconnect for #{self.server}:#{self.port}"
35
44
  self.connections.delete(self) if self.connections.include?(self)
36
45
  dispatch :client_disconnected
37
46
  Marvin::Loader.stop! if self.connections.blank?
@@ -51,11 +60,6 @@ module Marvin
51
60
  # that is more widely used throughout the client.
52
61
  def self.setup
53
62
  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
63
  if configuration.logger.blank?
60
64
  require 'logger'
61
65
  configuration.logger = Marvin::Logger.logger
@@ -80,40 +84,39 @@ module Marvin
80
84
  # to be in and if a password is specified in the configuration,
81
85
  # it will also attempt to identify us.
82
86
  def handle_client_connected(opts = {})
83
- logger.debug "About to handle post init"
87
+ logger.info "About to handle client connected"
88
+ # If the pass is set
89
+ unless self.pass.blank?
90
+ logger.info "Sending pass for connection"
91
+ command :pass, self.pass
92
+ end
84
93
  # 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
94
+ logger.info "Setting default nickname"
95
+ default_nickname = self.nicks.shift
87
96
  nick default_nickname
88
- logger.debug "sending user command"
97
+ logger.info "sending user command"
89
98
  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
99
  rescue Exception => e
96
100
  Marvin::ExceptionTracker.log(e)
97
101
  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
102
+
103
+ def default_channels
104
+ @default_channels ||= []
105
+ end
106
+
107
+ def default_channels=(channels)
108
+ @default_channels = channels.to_a.map { |c| c.to_s }
109
+ end
110
+
111
+ def nicks
112
+ if @nicks.blank? && !@nicks_loaded
113
+ logger.info "Setting default nick list"
114
+ @nicks = []
115
+ @nicks << self.configuration.nick
116
+ @nicks += self.configuration.nicks.to_a unless self.configuration.nicks.blank?
117
+ @nicks_loaded
116
118
  end
119
+ return @nicks
117
120
  end
118
121
 
119
122
  # The default response for PING's - it simply replies
@@ -126,9 +129,43 @@ module Marvin
126
129
  # TODO: Get the correct mapping for a given
127
130
  # Code.
128
131
  def handle_incoming_numeric(opts = {})
132
+ case opts[:code]
133
+ when Marvin::IRC::Replies[:RPL_WELCOME]
134
+ handle_welcome
135
+ when Marvin::IRC::Replies[:ERR_NICKNAMEINUSE]
136
+ handle_nick_taken
137
+ end
129
138
  code = opts[:code].to_i
130
139
  args = Marvin::Util.arguments(opts[:data])
131
- dispatch :incoming_numeric_processed, {:code => code, :data => args}
140
+ dispatch :incoming_numeric_processed, :code => code, :data => args
141
+ end
142
+
143
+ def handle_welcome
144
+ logger.info "Say hello to my little friend - Got welcome"
145
+ # If a password is specified, we will attempt to message
146
+ # NickServ to identify ourselves.
147
+ say ":IDENTIFY #{self.configuration.password}", "NickServ" unless self.configuration.password.blank?
148
+ # Join the default channels IF they're already set
149
+ # Note that Marvin::IRC::Client.connect will set them AFTER this stuff is run.
150
+ self.default_channels.each { |c| self.join(c) }
151
+ end
152
+
153
+ # The default handler for when a users nickname is taken on
154
+ # on the server. It will attempt to get the nicknickname from
155
+ # the nicknames part of the configuration (if available) and
156
+ # will then call #nick to change the nickname.
157
+ def handle_nick_taken
158
+ logger.info "Nickname '#{self.nickname}' on #{self.server} taken, trying next."
159
+ logger.info "Available Nicknames: #{self.nicks.empty? ? "None" : self.nicks.join(", ")}"
160
+ if !self.nicks.empty?
161
+ logger.info "Getting next nickname to switch"
162
+ next_nick = self.nicks.shift # Get the next nickname
163
+ logger.info "Attemping to set nickname to '#{next_nick}'"
164
+ nick next_nick
165
+ else
166
+ logger.fatal "No Nicknames available - QUITTING"
167
+ quit
168
+ end
132
169
  end
133
170
 
134
171
  ## General IRC Functions
@@ -141,7 +178,7 @@ module Marvin
141
178
  # First, get the appropriate command
142
179
  name = name.to_s.upcase
143
180
  args = args.flatten.compact
144
- irc_command = "#{name} #{args.join(" ").strip} \r\n"
181
+ irc_command = "#{name} #{args.join(" ").strip}\r\n"
145
182
  send_line irc_command
146
183
  end
147
184
 
@@ -166,13 +203,13 @@ module Marvin
166
203
  end
167
204
 
168
205
  def quit(reason = nil)
169
- logger.debug "Preparing to part from #{self.channels.size} channels"
206
+ logger.info "Preparing to part from #{self.channels.size} channels"
170
207
  self.channels.to_a.each do |chan|
171
- logger.debug "Parting from #{chan}"
208
+ logger.info "Parting from #{chan}"
172
209
  self.part chan, reason
173
210
  end
174
- logger.debug "Parted from all channels, quitting"
175
- command :quit
211
+ logger.info "Parted from all channels, quitting"
212
+ command :quit
176
213
  dispatch :quit
177
214
  # Remove the connections from the pool
178
215
  self.connections.delete(self)