agile-isaac 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +6 -0
- data/README.rdoc +94 -0
- data/isaac.gemspec +19 -0
- data/lib/isaac/config.rb +101 -0
- data/lib/isaac.rb +307 -0
- metadata +60 -0
data/LICENSE
ADDED
@@ -0,0 +1,6 @@
|
|
1
|
+
------------------------------------------------------------------------------
|
2
|
+
"THE BEER-WARE LICENSE" (Revision 42):
|
3
|
+
<ichverstehe@gmail.com> wrote this file. As long as you retain this notice you
|
4
|
+
can do whatever you want with this stuff. If we meet some day, and you think
|
5
|
+
this stuff is worth it, you can buy me a beer in return.
|
6
|
+
------------------------------------------------------------------------------
|
data/README.rdoc
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
= Isaac - the smallish DSL for writing IRC bots
|
2
|
+
You want to create an IRC bot quickly? Then Isaac is you. It will be. At some point, at least. But you shall be welcome to try it out and help me extend and beautify it. Be aware, the code is not stellar by any measure, most likely it is very crude and a large portion of the IRC standard has not been implemented, simply because I haven't needed it yet. Oh, and a lot of concepts were borrowed from Sinatra (http://sinatrarb.com). Thanks.
|
3
|
+
|
4
|
+
== Features
|
5
|
+
* Wraps parsing of incoming messages and raw IRC commands in simple constructs.
|
6
|
+
* Hides all the ugly regular expressions of matching IRC commands. Leaves only the essentials for you to match.
|
7
|
+
* Takes care of dull stuff such as replying to PING-messages and avoiding excess flood.
|
8
|
+
|
9
|
+
== Getting started
|
10
|
+
An Isaac-bot needs a few basics:
|
11
|
+
require 'isaac'
|
12
|
+
config do |c|
|
13
|
+
c.nick = "AwesomeBot"
|
14
|
+
c.server = "irc.freenode.net"
|
15
|
+
c.port = 6667
|
16
|
+
end
|
17
|
+
That's it. Run <tt>ruby bot.rb</tt> and it will connect to the specified server.
|
18
|
+
|
19
|
+
=== Connecting
|
20
|
+
After the bot has connected to the IRC server you might want to join some channels:
|
21
|
+
on :connect do
|
22
|
+
join "#awesome_channel", "#WesternBar"
|
23
|
+
end
|
24
|
+
|
25
|
+
=== Responding to messages
|
26
|
+
Joining a channel and sitting idle is not much fun. Let's repeat everything being said in these channels:
|
27
|
+
|
28
|
+
on :channel, /.*/ do
|
29
|
+
msg channel, message
|
30
|
+
end
|
31
|
+
|
32
|
+
Notice the +channel+ and +message+ variables. Additionally +nick+ and +match+ is available for channel-events. +nick+ being the sender of the message, +match+ being a MatchData object returned by the regular expression you specified:
|
33
|
+
|
34
|
+
on :channel, /^quote this: (.*)/ do
|
35
|
+
msg channel, "Quote: '#{match[1]}' by #{nick}"
|
36
|
+
end
|
37
|
+
|
38
|
+
If you want to match private messages use the +on :private+ event:
|
39
|
+
|
40
|
+
on :private, /^login (\S+) (\S+)/ do
|
41
|
+
username = match[1]
|
42
|
+
password = match[2]
|
43
|
+
# do something to authorize or whatevz.
|
44
|
+
msg nick, "Login successful!"
|
45
|
+
end
|
46
|
+
|
47
|
+
=== Defining helpers
|
48
|
+
Helpers should not be defined in the top level, but instead using the +helpers+-constructor:
|
49
|
+
|
50
|
+
helpers do
|
51
|
+
def rain_check(meeting)
|
52
|
+
msg nick, "Can I have a rain check on the #{meeting}?"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
on :private, /date/ do
|
57
|
+
rain_check("romantic date")
|
58
|
+
end
|
59
|
+
|
60
|
+
=== Errors, errors, errors
|
61
|
+
Errors, as specified by RFC 1459, can be reacted upon as well. If you e.g. try to send a message to a non-existant nick you will get error 401: "No such nick/channel".
|
62
|
+
|
63
|
+
on :error, 401 do
|
64
|
+
# Do something.
|
65
|
+
end
|
66
|
+
|
67
|
+
Available variables: +nick+ and +channel+.
|
68
|
+
|
69
|
+
=== Send commands from outside an event
|
70
|
+
You might want to send messages, join channels etc. without it strictly being the result of an on()-event, e.g. send a message every time a RSS feed is updated or whatever. You can use +Isaac.execute+ for that, and all your normal commands, +msg+, +join+, +topic+ etc. will be available:
|
71
|
+
|
72
|
+
class K
|
73
|
+
def smoke(brand)
|
74
|
+
Isaac.execute { msg "harryjr", "you should smoke #{brand} cigarettes" }
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
on :connect do
|
79
|
+
k = K.new
|
80
|
+
k.smoke("Lucky Strike")
|
81
|
+
end
|
82
|
+
|
83
|
+
== Contribute
|
84
|
+
The source is hosted at GitHub: http://github.com/ichverstehe/isaac
|
85
|
+
|
86
|
+
== License
|
87
|
+
------------------------------------------------------------------------------
|
88
|
+
"THE BEER-WARE LICENSE" (Revision 42):
|
89
|
+
<ichverstehe@gmail.com> wrote this file. As long as you retain this notice you
|
90
|
+
can do whatever you want with this stuff. If we meet some day, and you think
|
91
|
+
this stuff is worth it, you can buy me a beer in return.
|
92
|
+
------------------------------------------------------------------------------
|
93
|
+
|
94
|
+
|
data/isaac.gemspec
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = "isaac"
|
3
|
+
s.version = "0.0.4"
|
4
|
+
s.date = "2008-12-17"
|
5
|
+
s.summary = "The smallish DSL for writing IRC bots"
|
6
|
+
s.email = "mike@cryingwhilecoding.com"
|
7
|
+
s.homepage = "http://github.com/agile/isaac"
|
8
|
+
s.description = "Small DSL for writing IRC bots."
|
9
|
+
s.rubyforge_project = "isaac"
|
10
|
+
s.has_rdoc = true
|
11
|
+
s.authors = ["Harry Vangberg", "Mike Vincent"]
|
12
|
+
s.files = ["README.rdoc",
|
13
|
+
"LICENSE",
|
14
|
+
"isaac.gemspec",
|
15
|
+
"lib/isaac/config.rb",
|
16
|
+
"lib/isaac.rb"]
|
17
|
+
s.rdoc_options = ["--main", "README.rdoc"]
|
18
|
+
s.extra_rdoc_files = ["LICENSE", "README.rdoc"]
|
19
|
+
end
|
data/lib/isaac/config.rb
ADDED
@@ -0,0 +1,101 @@
|
|
1
|
+
module Isaac
|
2
|
+
module Config
|
3
|
+
class << self
|
4
|
+
def data
|
5
|
+
@data
|
6
|
+
end
|
7
|
+
|
8
|
+
def data=(data)
|
9
|
+
@data = data
|
10
|
+
self.dirty = false
|
11
|
+
end
|
12
|
+
alias :config= :data=
|
13
|
+
|
14
|
+
def config(network=nil)
|
15
|
+
network ||= default_network
|
16
|
+
@data.config[network] || {}
|
17
|
+
end
|
18
|
+
|
19
|
+
def networks
|
20
|
+
@data.networks
|
21
|
+
end
|
22
|
+
|
23
|
+
def default_network
|
24
|
+
networks.include?('default') ? 'default' : networks.first
|
25
|
+
end
|
26
|
+
|
27
|
+
def loaded?
|
28
|
+
!data.nil?
|
29
|
+
end
|
30
|
+
|
31
|
+
def dirty?
|
32
|
+
@dirty == true
|
33
|
+
end
|
34
|
+
|
35
|
+
def dirty=(bool)
|
36
|
+
@dirty = bool
|
37
|
+
end
|
38
|
+
|
39
|
+
def write_config(filename=nil)
|
40
|
+
File.open(filename || data.config_filename, 'w') do |file|
|
41
|
+
file << data.to_yaml
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def method_missing(method, *args)
|
46
|
+
if method.to_s =~ /=$/
|
47
|
+
base_name = method.to_s.gsub(/=$/,'')
|
48
|
+
self.dirty = true
|
49
|
+
data.config[default_network][base_name] = args.first
|
50
|
+
elsif config.has_key?(method.to_s)
|
51
|
+
config[method.to_s]
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
class Data
|
56
|
+
attr_accessor :config, :config_filename
|
57
|
+
|
58
|
+
def initialize(config=nil)
|
59
|
+
self.config = read_config(config)
|
60
|
+
self.config_filename = config || default_config_file
|
61
|
+
end
|
62
|
+
|
63
|
+
def config_dir
|
64
|
+
File.exist?('config') && File.directory?('config') ? './config' : '.'
|
65
|
+
end
|
66
|
+
|
67
|
+
def default_config_file
|
68
|
+
File.join(config_dir, "config.yml")
|
69
|
+
end
|
70
|
+
|
71
|
+
def networks
|
72
|
+
config.keys
|
73
|
+
end
|
74
|
+
|
75
|
+
def read_config(config=nil)
|
76
|
+
begin
|
77
|
+
require 'erb'
|
78
|
+
require 'yaml'
|
79
|
+
rescue LoadError
|
80
|
+
retry if require 'rubygems'
|
81
|
+
end
|
82
|
+
begin
|
83
|
+
config ||= config_filename
|
84
|
+
self.config_filename = config unless config == config_filename
|
85
|
+
YAML::load(ERB.new(IO.read(config)).result)
|
86
|
+
rescue
|
87
|
+
{"default" => {}}
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def to_yaml
|
92
|
+
config.to_yaml
|
93
|
+
end
|
94
|
+
|
95
|
+
def self.load(config=nil)
|
96
|
+
Isaac::Config.data = new(config)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
data/lib/isaac.rb
ADDED
@@ -0,0 +1,307 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'isaac/config'
|
3
|
+
require 'logger'
|
4
|
+
|
5
|
+
module Isaac
|
6
|
+
|
7
|
+
# Returns the current instance of Isaac::Application
|
8
|
+
def self.app
|
9
|
+
@app ||= Application.new
|
10
|
+
end
|
11
|
+
|
12
|
+
# Use EventContext methods such as msg(), join() etc. outside on()-events. See +examples/execute.rb+.
|
13
|
+
# Isaac.execute do
|
14
|
+
# msg 'harryjr', 'you're awesome'
|
15
|
+
# end
|
16
|
+
def self.execute(params={}, &block)
|
17
|
+
app.execute(params, &block)
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.logger
|
21
|
+
@logger ||= Logger.new(Config.log_file || "isaac.log")
|
22
|
+
end
|
23
|
+
|
24
|
+
#Config = Struct.new(:nick, :server, :port, :server_pass, :username, :realname, :version, :verbose, :nick_pass)
|
25
|
+
|
26
|
+
# These are top level methods you use to construct your bot.
|
27
|
+
class Application
|
28
|
+
def initialize #:nodoc:
|
29
|
+
@events = Hash.new {|k,v| k[v] = []}
|
30
|
+
@registration = []
|
31
|
+
end
|
32
|
+
|
33
|
+
def logger
|
34
|
+
Isaac.logger
|
35
|
+
end
|
36
|
+
|
37
|
+
# This is plain stupid. Might be useful for logging or something later on.
|
38
|
+
def start #:nodoc:
|
39
|
+
logger.info " ==== Starting Isaac ==== "
|
40
|
+
connect
|
41
|
+
logger.info " ==== Ending Isaac ==== "
|
42
|
+
end
|
43
|
+
|
44
|
+
# Configure the bot:
|
45
|
+
# defaults to the 'default' or first network
|
46
|
+
#
|
47
|
+
# config do |c|
|
48
|
+
# c.server = "irc.freenode.net"
|
49
|
+
# c.nick = "AwesomeBot"
|
50
|
+
# c.port = 6667
|
51
|
+
# c.realname = "James Dean"
|
52
|
+
# c.username = "jdean"
|
53
|
+
# c.version = "James Dean Bot v2.34"
|
54
|
+
# c.verbose = true
|
55
|
+
# c.server_pass = "secrets_for_server_connections"
|
56
|
+
# c.nick_pass = "secrets_for_nickserv"
|
57
|
+
# end
|
58
|
+
#
|
59
|
+
# config("mynet") do |c|
|
60
|
+
# c.server = "irc.example.com"
|
61
|
+
# c.port = 6667
|
62
|
+
# c.nick = "larry"
|
63
|
+
# end
|
64
|
+
#
|
65
|
+
# Can also be used as an accessor to configuration:
|
66
|
+
# config.nick => "AwesomeBot"
|
67
|
+
# config("mynet").nick => "larry"
|
68
|
+
#
|
69
|
+
def config(&block)
|
70
|
+
Config::Data.load unless Config.loaded?
|
71
|
+
block.call(Config) if block_given?
|
72
|
+
Config
|
73
|
+
end
|
74
|
+
|
75
|
+
# Methods defined inside the helpers-block will be available to on()-events at execution time.
|
76
|
+
def helpers(&block)
|
77
|
+
EventContext.class_eval(&block)
|
78
|
+
end
|
79
|
+
|
80
|
+
# on()-events responds to certain actions. Depending on +type+ certain local variables are available:
|
81
|
+
# +nick+, +channel+, +message+ and in particular +match+, which contains a MatchData object returned
|
82
|
+
# by the given regular expression.
|
83
|
+
#
|
84
|
+
# * Do something after connection has been established, e.g. join channels.
|
85
|
+
# on :connect do
|
86
|
+
# join "#awesome_channel", "#lee_marvin_fans"
|
87
|
+
# end
|
88
|
+
# * Respond to private messages matching a given regular expression.
|
89
|
+
# on :private, /^echo (.*)/ do
|
90
|
+
# msg nick, "You said '#{match[1]}!"
|
91
|
+
# end
|
92
|
+
# * Respond to messages matching a given regular expression send to a channel.
|
93
|
+
# on :channel, /quote/ do
|
94
|
+
# msg channel, "#{nick} requested a quote: 'Smoking, a subtle form a suicide.' - Vonnegut"
|
95
|
+
# end
|
96
|
+
# * Respond to error codes, according to the RFC.
|
97
|
+
# on :error, 401 do
|
98
|
+
# # Execute this if you try to send a message to a non-existing nick/channel.
|
99
|
+
# end
|
100
|
+
def on(type, match=nil, &block)
|
101
|
+
@events[type] << e = Event.new(match, block)
|
102
|
+
return e
|
103
|
+
end
|
104
|
+
|
105
|
+
def execute(params={}, &block) #:nodoc:
|
106
|
+
event = Event.new(:dsl, block)
|
107
|
+
@queue << event.invoke(params)
|
108
|
+
end
|
109
|
+
|
110
|
+
def event(type, matcher)
|
111
|
+
@events[type].detect do |e|
|
112
|
+
type == :error ? matcher == e.match : matcher =~ e.match
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def connect
|
117
|
+
begin
|
118
|
+
logger.info "Connecting to #{config.server} at port #{config.port}"
|
119
|
+
@irc = TCPSocket.open(config.server, config.port)
|
120
|
+
logger.info "Connection established."
|
121
|
+
|
122
|
+
@irc.puts "PASS #{config.server_pass}" if config.server_pass
|
123
|
+
@irc.puts "NICK #{config.nick}"
|
124
|
+
@irc.puts "USER #{config.username} foobar foobar :#{config.realname}"
|
125
|
+
|
126
|
+
@queue = Queue.new(@irc)
|
127
|
+
@queue << @events[:connect].first.invoke if @events[:connect].first
|
128
|
+
|
129
|
+
while line = @irc.gets
|
130
|
+
handle line
|
131
|
+
end
|
132
|
+
rescue Interrupt => e
|
133
|
+
puts "Disconnected! An error occurred: #{e.inspect}"
|
134
|
+
#rescue Timeout::Error => e
|
135
|
+
# puts "Timeout: #{e}. Reconnecting."
|
136
|
+
# connect
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def registered?
|
141
|
+
arr = [1,2,3,4] - @registration
|
142
|
+
arr.empty?
|
143
|
+
end
|
144
|
+
|
145
|
+
# This is one hell of a nasty method. Something should be done, I suppose.
|
146
|
+
def handle(line)
|
147
|
+
puts "> #{line}" if config.verbose
|
148
|
+
|
149
|
+
case line
|
150
|
+
when /^:(\S+)!\S+ PRIVMSG \S+ :?\001VERSION\001/
|
151
|
+
@queue << "NOTICE #{$1} :\001VERSION #{config.version}\001"
|
152
|
+
when /^:(\S+)!(\S+) PRIVMSG (\S+) :?(.*)/
|
153
|
+
nick, userhost, channel, message = $1, $2, $3, $4
|
154
|
+
type = channel.match(/^#/) ? :channel : :private
|
155
|
+
if event = event(type, message)
|
156
|
+
@queue << event.invoke(:nick => nick, :userhost => userhost, :channel => channel, :message => message)
|
157
|
+
end
|
158
|
+
when /^:\S+ 00([1-4])/
|
159
|
+
@registration << $1.to_i
|
160
|
+
@queue.lock = false if registered?
|
161
|
+
when /^:\S+ ([4-5]\d\d) \S+ (\S+)/
|
162
|
+
error = $1
|
163
|
+
nick = channel = $2
|
164
|
+
if event = event(:error, error.to_i)
|
165
|
+
@queue << event.invoke(:nick => nick, :channel => channel)
|
166
|
+
end
|
167
|
+
when /^PING (\S+)/
|
168
|
+
#TODO not sure this is correct. Damned RFC.
|
169
|
+
@queue << "PONG #{$1}"
|
170
|
+
when /^:\S+ PONG \S+ :excess/
|
171
|
+
@queue.lock = false
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
class Queue #:nodoc:
|
177
|
+
attr_accessor :lock
|
178
|
+
def initialize(socket)
|
179
|
+
@socket = socket
|
180
|
+
@queue = []
|
181
|
+
@transfered = 0
|
182
|
+
@lock = true
|
183
|
+
transmit
|
184
|
+
end
|
185
|
+
|
186
|
+
# I luvz Rubyz
|
187
|
+
def << (msg)
|
188
|
+
# .flatten! returns nill if no modifications were made, thus we do this.
|
189
|
+
@queue = (@queue << msg).flatten
|
190
|
+
end
|
191
|
+
|
192
|
+
# To prevent excess flood no more than 1472 bytes will be sent to the
|
193
|
+
# server. When that limit is reached, @lock = true and the server will be
|
194
|
+
# PINGed. @lock will be true until a PONG is received (Application#handle).
|
195
|
+
def transmit
|
196
|
+
Thread.start { loop {
|
197
|
+
unless @lock || @queue.empty?
|
198
|
+
msg = @queue.first
|
199
|
+
if (@transfered + msg.size) > 1472
|
200
|
+
# No honestly, :excess. The RFC is not too clear on this subject TODO
|
201
|
+
@socket.puts "PING :excess"
|
202
|
+
@lock = true
|
203
|
+
@transfered = 0
|
204
|
+
else
|
205
|
+
@socket.puts msg
|
206
|
+
@transfered += msg.size
|
207
|
+
@queue.shift
|
208
|
+
end
|
209
|
+
end
|
210
|
+
sleep 0.1
|
211
|
+
}}
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
class Event #:nodoc:
|
216
|
+
attr_accessor :match, :block
|
217
|
+
def initialize(match, block)
|
218
|
+
@match = match
|
219
|
+
@block = block
|
220
|
+
end
|
221
|
+
|
222
|
+
# Execute event in the context of EventContext.
|
223
|
+
def invoke(params={})
|
224
|
+
match = params[:message].match(@match) if @match && params[:message]
|
225
|
+
params.merge!(:match => match)
|
226
|
+
|
227
|
+
context = EventContext.new(params)
|
228
|
+
context.instance_eval(&@block)
|
229
|
+
context.commands
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
class EventContext
|
234
|
+
attr_accessor :nick, :userhost, :channel, :message, :match, :commands
|
235
|
+
def initialize(args = {})
|
236
|
+
args.each {|k,v| instance_variable_set("@#{k}",v)}
|
237
|
+
@commands = []
|
238
|
+
end
|
239
|
+
|
240
|
+
# Send a raw IRC message.
|
241
|
+
def raw(command)
|
242
|
+
@commands << command
|
243
|
+
end
|
244
|
+
|
245
|
+
# Send a message to nick/channel.
|
246
|
+
def msg(recipient, text)
|
247
|
+
raw("PRIVMSG #{recipient} :#{text}")
|
248
|
+
end
|
249
|
+
|
250
|
+
# Send a notice to nick/channel
|
251
|
+
def notice(recipient, text)
|
252
|
+
raw("PRIVMSG #{recipient} :#{text}")
|
253
|
+
end
|
254
|
+
|
255
|
+
# Join channel(s):
|
256
|
+
# join "#awesome_channel"
|
257
|
+
# join "#rollercoaster", "#j-lo"
|
258
|
+
def join(*channels)
|
259
|
+
channels.each {|channel| raw("JOIN #{channel}")}
|
260
|
+
end
|
261
|
+
|
262
|
+
# Part channel(s):
|
263
|
+
# part "#awesome_channel"
|
264
|
+
# part "#rollercoaster", "#j-lo"
|
265
|
+
def part(*channels)
|
266
|
+
channels.each {|channel| raw("PART #{channel}")}
|
267
|
+
end
|
268
|
+
|
269
|
+
# Kick nick from channel, with optional comment.
|
270
|
+
def kick(channel, nick, comment=nil)
|
271
|
+
comment = " :#{comment}" if comment
|
272
|
+
raw("KICK #{channel} #{nick}#{comment}")
|
273
|
+
end
|
274
|
+
|
275
|
+
# Change topic of channel.
|
276
|
+
def topic(channel, topic)
|
277
|
+
raw("TOPIC #{channel} :#{topic}")
|
278
|
+
end
|
279
|
+
|
280
|
+
# Invite nicks to channel
|
281
|
+
# invite "#awesome_channel", "arnie"
|
282
|
+
# invite "#awesome_channel", "arnie", "brigitte"
|
283
|
+
def invite(channel, *nicks)
|
284
|
+
nicks.each {|nick| raw("INVITE #{nick} #{channel}")}
|
285
|
+
end
|
286
|
+
|
287
|
+
# Change nickname
|
288
|
+
def newnick(nickname)
|
289
|
+
raw("NICK #{nickname}")
|
290
|
+
end
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
# Assign methods to current Isaac instance
|
295
|
+
%w(config helpers on).each do |method|
|
296
|
+
eval(<<-EOF)
|
297
|
+
def #{method}(*args, &block)
|
298
|
+
Isaac.app.#{method}(*args, &block)
|
299
|
+
end
|
300
|
+
EOF
|
301
|
+
end
|
302
|
+
|
303
|
+
# Clever, thanks Sinatra.
|
304
|
+
at_exit do
|
305
|
+
raise $! if $!
|
306
|
+
Isaac.app.start
|
307
|
+
end
|
metadata
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: agile-isaac
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.4
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Harry Vangberg
|
8
|
+
- Mike Vincent
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
|
13
|
+
date: 2008-12-17 00:00:00 -08:00
|
14
|
+
default_executable:
|
15
|
+
dependencies: []
|
16
|
+
|
17
|
+
description: Small DSL for writing IRC bots.
|
18
|
+
email: mike@cryingwhilecoding.com
|
19
|
+
executables: []
|
20
|
+
|
21
|
+
extensions: []
|
22
|
+
|
23
|
+
extra_rdoc_files:
|
24
|
+
- LICENSE
|
25
|
+
- README.rdoc
|
26
|
+
files:
|
27
|
+
- README.rdoc
|
28
|
+
- LICENSE
|
29
|
+
- isaac.gemspec
|
30
|
+
- lib/isaac/config.rb
|
31
|
+
- lib/isaac.rb
|
32
|
+
has_rdoc: true
|
33
|
+
homepage: http://github.com/agile/isaac
|
34
|
+
post_install_message:
|
35
|
+
rdoc_options:
|
36
|
+
- --main
|
37
|
+
- README.rdoc
|
38
|
+
require_paths:
|
39
|
+
- lib
|
40
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
41
|
+
requirements:
|
42
|
+
- - ">="
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
version: "0"
|
45
|
+
version:
|
46
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
47
|
+
requirements:
|
48
|
+
- - ">="
|
49
|
+
- !ruby/object:Gem::Version
|
50
|
+
version: "0"
|
51
|
+
version:
|
52
|
+
requirements: []
|
53
|
+
|
54
|
+
rubyforge_project: isaac
|
55
|
+
rubygems_version: 1.2.0
|
56
|
+
signing_key:
|
57
|
+
specification_version: 2
|
58
|
+
summary: The smallish DSL for writing IRC bots
|
59
|
+
test_files: []
|
60
|
+
|