Sutto-marvin 0.1.20081115 → 0.1.20081120
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.
- 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)
|