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.
- data/README.textile +70 -50
- data/VERSION.yml +1 -1
- data/bin/marvin +10 -6
- data/config/connections.yml.sample +5 -0
- data/config/settings.yml.sample +2 -7
- data/config/setup.rb +1 -0
- data/handlers/debug_handler.rb +5 -0
- data/handlers/hello_world.rb +1 -1
- data/lib/marvin.rb +1 -0
- data/lib/marvin/abstract_client.rb +79 -42
- data/lib/marvin/abstract_parser.rb +14 -2
- data/lib/marvin/base.rb +44 -6
- data/lib/marvin/dispatchable.rb +9 -4
- data/lib/marvin/drb_handler.rb +7 -2
- data/lib/marvin/exceptions.rb +3 -0
- data/lib/marvin/irc.rb +1 -1
- data/lib/marvin/irc/client.rb +31 -7
- data/lib/marvin/irc/event.rb +9 -4
- data/lib/marvin/irc/replies.rb +154 -0
- data/lib/marvin/loader.rb +1 -0
- data/lib/marvin/logger.rb +66 -3
- data/lib/marvin/options.rb +33 -0
- data/lib/marvin/parsers.rb +3 -0
- data/lib/marvin/parsers/command.rb +73 -0
- data/lib/marvin/parsers/prefixes.rb +8 -0
- data/lib/marvin/parsers/prefixes/host_mask.rb +30 -0
- data/lib/marvin/parsers/prefixes/server.rb +24 -0
- data/lib/marvin/parsers/ragel_parser.rb +713 -0
- data/lib/marvin/parsers/ragel_parser.rl +144 -0
- data/lib/marvin/parsers/regexp_parser.rb +0 -3
- data/lib/marvin/parsers/simple_parser.rb +20 -81
- data/lib/marvin/settings.rb +8 -8
- data/lib/marvin/util.rb +2 -2
- data/script/{run → client} +0 -0
- data/script/daemon-runner +1 -1
- data/script/install +3 -0
- data/test/parser_comparison.rb +36 -0
- data/test/parser_test.rb +20 -0
- data/test/test_helper.rb +10 -0
- metadata +19 -9
- data/lib/marvin/irc/socket_client.rb +0 -69
- data/lib/marvin/parsers/simple_parser/default_events.rb +0 -37
- data/lib/marvin/parsers/simple_parser/event_extensions.rb +0 -14
- 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
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
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:
|
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:
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
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
|
-
|
78
|
-
|
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
|
-
|
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:
|
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),
|
132
|
-
|
133
|
-
|
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", :
|
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
|
-
|
175
|
+
which will persist the count between each session.
|
data/VERSION.yml
CHANGED
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
|
46
|
-
copy "script/
|
48
|
+
puts "Copying start scripts"
|
49
|
+
copy "script/client"
|
47
50
|
copy "script/daemon-runner"
|
48
|
-
FileUtils.chmod 0755, j(DEST, "script/
|
51
|
+
FileUtils.chmod 0755, j(DEST, "script/client")
|
49
52
|
FileUtils.chmod 0755, j(DEST, "script/daemon-runner")
|
50
53
|
|
51
|
-
puts "Copying example
|
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/
|
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/
|
70
|
+
exec "script/client"
|
67
71
|
end
|
data/config/settings.yml.sample
CHANGED
@@ -1,13 +1,8 @@
|
|
1
1
|
default:
|
2
|
-
name:
|
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:
|
12
|
-
production:
|
13
|
-
deployed: false
|
8
|
+
nick: Marvin
|
data/config/setup.rb
CHANGED
data/handlers/hello_world.rb
CHANGED
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.
|
33
|
+
logger.info "Initializing the current instance"
|
26
34
|
self.channels = []
|
27
35
|
self.connections << self
|
28
|
-
logger.
|
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.
|
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.
|
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.
|
86
|
-
default_nickname = self.
|
94
|
+
logger.info "Setting default nickname"
|
95
|
+
default_nickname = self.nicks.shift
|
87
96
|
nick default_nickname
|
88
|
-
logger.
|
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
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
def
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
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,
|
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}
|
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.
|
206
|
+
logger.info "Preparing to part from #{self.channels.size} channels"
|
170
207
|
self.channels.to_a.each do |chan|
|
171
|
-
logger.
|
208
|
+
logger.info "Parting from #{chan}"
|
172
209
|
self.part chan, reason
|
173
210
|
end
|
174
|
-
logger.
|
175
|
-
command
|
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)
|