ichverstehe-isaac 0.0.4 → 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +10 -16
- data/isaac.gemspec +3 -3
- data/lib/isaac.rb +123 -248
- metadata +3 -3
data/README.rdoc
CHANGED
@@ -1,5 +1,4 @@
|
|
1
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
2
|
|
4
3
|
== Features
|
5
4
|
* Wraps parsing of incoming messages and raw IRC commands in simple constructs.
|
@@ -9,7 +8,7 @@ You want to create an IRC bot quickly? Then Isaac is you. It will be. At some po
|
|
9
8
|
== Getting started
|
10
9
|
An Isaac-bot needs a few basics:
|
11
10
|
require 'isaac'
|
12
|
-
|
11
|
+
configure do |c|
|
13
12
|
c.nick = "AwesomeBot"
|
14
13
|
c.server = "irc.freenode.net"
|
15
14
|
c.port = 6667
|
@@ -25,21 +24,23 @@ After the bot has connected to the IRC server you might want to join some channe
|
|
25
24
|
=== Responding to messages
|
26
25
|
Joining a channel and sitting idle is not much fun. Let's repeat everything being said in these channels:
|
27
26
|
|
28
|
-
on :channel,
|
27
|
+
on :channel, // do
|
29
28
|
msg channel, message
|
30
29
|
end
|
31
30
|
|
32
|
-
Notice the +channel+ and +message+ variables. Additionally +nick+ and +match+ is
|
31
|
+
Notice the +channel+ and +message+ variables. Additionally +nick+ and +match+ is
|
32
|
+
available for channel-events. +nick+ being the sender of the message, +match+
|
33
|
+
being an array of captures from the regular expression:
|
33
34
|
|
34
35
|
on :channel, /^quote this: (.*)/ do
|
35
|
-
msg channel, "Quote: '#{match[
|
36
|
+
msg channel, "Quote: '#{match[0]}' by #{nick}"
|
36
37
|
end
|
37
38
|
|
38
39
|
If you want to match private messages use the +on :private+ event:
|
39
40
|
|
40
41
|
on :private, /^login (\S+) (\S+)/ do
|
41
|
-
username = match[
|
42
|
-
password = match[
|
42
|
+
username = match[0]
|
43
|
+
password = match[0]
|
43
44
|
# do something to authorize or whatevz.
|
44
45
|
msg nick, "Login successful!"
|
45
46
|
end
|
@@ -66,7 +67,7 @@ Errors, as specified by RFC 1459, can be reacted upon as well. If you e.g. try t
|
|
66
67
|
|
67
68
|
Available variables: +nick+ and +channel+.
|
68
69
|
|
69
|
-
=== Send commands from outside an event
|
70
|
+
=== Send commands from outside an event (not implemented in Shaft atm)
|
70
71
|
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
|
|
72
73
|
class K
|
@@ -84,11 +85,4 @@ You might want to send messages, join channels etc. without it strictly being th
|
|
84
85
|
The source is hosted at GitHub: http://github.com/ichverstehe/isaac
|
85
86
|
|
86
87
|
== 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
|
-
|
88
|
+
The MIT. Google it.
|
data/isaac.gemspec
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
s.name = "isaac"
|
3
|
-
s.version = "0.
|
4
|
-
s.date = "2009-
|
3
|
+
s.version = "0.2.1"
|
4
|
+
s.date = "2009-02-23"
|
5
5
|
s.summary = "The smallish DSL for writing IRC bots"
|
6
|
-
s.email = "
|
6
|
+
s.email = "harry@vangberg.name"
|
7
7
|
s.homepage = "http://github.com/ichverstehe/isaac"
|
8
8
|
s.description = "Small DSL for writing IRC bots."
|
9
9
|
s.rubyforge_project = "isaac"
|
data/lib/isaac.rb
CHANGED
@@ -1,305 +1,180 @@
|
|
1
1
|
require 'socket'
|
2
|
+
|
2
3
|
module Isaac
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
end
|
4
|
+
VERSION = '0.2.1'
|
5
|
+
|
6
|
+
Config = Struct.new(:server, :port, :password, :nick, :realname, :version, :environment, :verbose)
|
7
7
|
|
8
|
-
|
9
|
-
|
10
|
-
# msg 'harryjr', 'you're awesome'
|
11
|
-
# end
|
12
|
-
def self.execute(params={}, &block)
|
13
|
-
app.execute(params, &block)
|
8
|
+
def self.bot
|
9
|
+
@bot ||= Bot.new
|
14
10
|
end
|
15
11
|
|
16
|
-
|
12
|
+
class Bot
|
13
|
+
attr_accessor :config, :irc, :nick, :channel, :message, :userhost, :match,
|
14
|
+
:error
|
17
15
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
@events = Hash.new {|k,v| k[v] = []}
|
22
|
-
@registration = []
|
23
|
-
end
|
16
|
+
def initialize(&b)
|
17
|
+
@events = {}
|
18
|
+
@config = Config.new("localhost", 6667, nil, "isaac", "Isaac", 'isaac', :production, false)
|
24
19
|
|
25
|
-
|
26
|
-
def start #:nodoc:
|
27
|
-
puts " ==== Starting Isaac ==== "
|
28
|
-
connect
|
29
|
-
puts " ==== Ending Isaac ==== "
|
20
|
+
instance_eval(&b) if block_given?
|
30
21
|
end
|
31
22
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
# c.nick = "AwesomeBot"
|
36
|
-
# c.port = 6667
|
37
|
-
# c.realname = "James Dean"
|
38
|
-
# c.username = "jdean"
|
39
|
-
# c.version = "James Dean Bot v2.34"
|
40
|
-
# c.verbose = true
|
41
|
-
# end
|
42
|
-
def config(&block)
|
43
|
-
@config = Config.new('isaac_bot', 'irc.freenode.net', 6667, 'isaac', 'isaac', 'isaac-bot', false)
|
44
|
-
block.call(@config)
|
45
|
-
@config
|
23
|
+
def start
|
24
|
+
@irc = IRC.new(self, @config)
|
25
|
+
@irc.connect
|
46
26
|
end
|
47
27
|
|
48
|
-
|
49
|
-
|
50
|
-
|
28
|
+
def on(event, match=//, &b)
|
29
|
+
match = match.to_s if match.is_a? Integer
|
30
|
+
(@events[event] ||= []) << [Regexp.new(match), b]
|
51
31
|
end
|
52
32
|
|
53
|
-
|
54
|
-
|
55
|
-
# by the given regular expression.
|
56
|
-
#
|
57
|
-
# * Do something after connection has been established, e.g. join channels.
|
58
|
-
# on :connect do
|
59
|
-
# join "#awesome_channel", "#lee_marvin_fans"
|
60
|
-
# end
|
61
|
-
# * Respond to private messages matching a given regular expression.
|
62
|
-
# on :private, /^echo (.*)/ do
|
63
|
-
# msg nick, "You said '#{match[1]}!"
|
64
|
-
# end
|
65
|
-
# * Respond to messages matching a given regular expression send to a channel.
|
66
|
-
# on :channel, /quote/ do
|
67
|
-
# msg channel, "#{nick} requested a quote: 'Smoking, a subtle form a suicide.' - Vonnegut"
|
68
|
-
# end
|
69
|
-
# * Respond to error codes, according to the RFC.
|
70
|
-
# on :error, 401 do
|
71
|
-
# # Execute this if you try to send a message to a non-existing nick/channel.
|
72
|
-
# end
|
73
|
-
def on(type, match=nil, &block)
|
74
|
-
@events[type] << e = Event.new(match, block)
|
75
|
-
return e
|
33
|
+
def helpers(&b)
|
34
|
+
instance_eval &b
|
76
35
|
end
|
77
36
|
|
78
|
-
def
|
79
|
-
|
80
|
-
@queue << event.invoke(params)
|
37
|
+
def configure(&b)
|
38
|
+
b.call(@config)
|
81
39
|
end
|
82
40
|
|
83
|
-
def event
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
end
|
88
|
-
|
89
|
-
def connect
|
90
|
-
begin
|
91
|
-
puts "Connecting to #{@config.server} at port #{@config.port}"
|
92
|
-
@irc = TCPSocket.open(@config.server, @config.port)
|
93
|
-
puts "Connection established."
|
94
|
-
|
95
|
-
@irc.puts "PASS #{@config.password}" if @config.password
|
96
|
-
@irc.puts "NICK #{@config.nick}"
|
97
|
-
@irc.puts "USER #{@config.username} foobar foobar :#{@config.realname}"
|
41
|
+
def dispatch(event, env={})
|
42
|
+
self.nick, self.userhost, self.channel, self.error =
|
43
|
+
env[:nick], env[:userhost], env[:channel], env[:error]
|
44
|
+
self.message = env[:message] || ""
|
98
45
|
|
99
|
-
|
100
|
-
|
46
|
+
event = @events[event] && @events[event].detect do |regexp,_|
|
47
|
+
message.match(regexp)
|
48
|
+
end
|
101
49
|
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
puts "Disconnected! An error occurred: #{e.inspect}"
|
107
|
-
rescue Timeout::Error => e
|
108
|
-
puts "Timeout: #{e}. Reconnecting."
|
109
|
-
connect
|
50
|
+
if event
|
51
|
+
regexp, block = *event
|
52
|
+
self.match = message.match(regexp).captures
|
53
|
+
catch(:halt) { instance_eval(&block) }
|
110
54
|
end
|
111
55
|
end
|
112
56
|
|
113
|
-
def
|
114
|
-
|
115
|
-
arr.empty?
|
57
|
+
def halt
|
58
|
+
throw :halt
|
116
59
|
end
|
117
60
|
|
118
|
-
|
119
|
-
|
120
|
-
puts "> #{line}" if @config.verbose
|
121
|
-
|
122
|
-
case line
|
123
|
-
when /^:(\S+)!\S+ PRIVMSG \S+ :?\001VERSION\001/
|
124
|
-
@queue << "NOTICE #{$1} :\001VERSION #{@config.version}\001"
|
125
|
-
when /^:(\S+)!(\S+) PRIVMSG (\S+) :?(.*)/
|
126
|
-
nick, userhost, channel, message = $1, $2, $3, $4
|
127
|
-
type = channel.match(/^#/) ? :channel : :private
|
128
|
-
if event = event(type, message)
|
129
|
-
@queue << event.invoke(:nick => nick, :userhost => userhost, :channel => channel, :message => message)
|
130
|
-
end
|
131
|
-
when /^:\S+ 00([1-4])/
|
132
|
-
@registration << $1.to_i
|
133
|
-
@queue.lock = false if registered?
|
134
|
-
when /^:\S+ ([4-5]\d\d) \S+ (\S+)/
|
135
|
-
error = $1
|
136
|
-
nick = channel = $2
|
137
|
-
if event = event(:error, error.to_i)
|
138
|
-
@queue << event.invoke(:nick => nick, :channel => channel)
|
139
|
-
end
|
140
|
-
when /^PING (\S+)/
|
141
|
-
#TODO not sure this is correct. Damned RFC.
|
142
|
-
if registered?
|
143
|
-
@queue << "PONG #{$1}"
|
144
|
-
else
|
145
|
-
@irc.puts "PONG #{$1}"
|
146
|
-
end
|
147
|
-
when /^:\S+ PONG \S+ :excess/
|
148
|
-
@queue.lock = false
|
149
|
-
end
|
150
|
-
end
|
151
|
-
end
|
152
|
-
|
153
|
-
class Queue #:nodoc:
|
154
|
-
attr_accessor :lock
|
155
|
-
def initialize(socket)
|
156
|
-
@socket = socket
|
157
|
-
@queue = []
|
158
|
-
@transfered = 0
|
159
|
-
@lock = true
|
160
|
-
transmit
|
61
|
+
def raw(m)
|
62
|
+
@irc.message(m)
|
161
63
|
end
|
162
64
|
|
163
|
-
|
164
|
-
|
165
|
-
# .flatten! returns nill if no modifications were made, thus we do this.
|
166
|
-
@queue = (@queue << msg).flatten
|
65
|
+
def msg(recipient, m)
|
66
|
+
raw("PRIVMSG #{recipient} :#{m}")
|
167
67
|
end
|
168
68
|
|
169
|
-
|
170
|
-
|
171
|
-
# PINGed. @lock will be true until a PONG is received (Application#handle).
|
172
|
-
def transmit
|
173
|
-
Thread.start { loop {
|
174
|
-
unless @lock || @queue.empty?
|
175
|
-
msg = @queue.first
|
176
|
-
if (@transfered + msg.size) > 1472
|
177
|
-
# No honestly, :excess. The RFC is not too clear on this subject TODO
|
178
|
-
@socket.puts "PING :excess"
|
179
|
-
@lock = true
|
180
|
-
@transfered = 0
|
181
|
-
else
|
182
|
-
@socket.puts msg
|
183
|
-
@transfered += msg.size
|
184
|
-
@queue.shift
|
185
|
-
end
|
186
|
-
end
|
187
|
-
sleep 0.1
|
188
|
-
}}
|
69
|
+
def join(*channels)
|
70
|
+
channels.each {|channel| raw("JOIN #{channel}")}
|
189
71
|
end
|
190
|
-
end
|
191
72
|
|
192
|
-
|
193
|
-
|
194
|
-
def initialize(match, block)
|
195
|
-
@match = match
|
196
|
-
@block = block
|
73
|
+
def part(*channels)
|
74
|
+
channels.each {|channel| raw("PART #{channel}")}
|
197
75
|
end
|
198
76
|
|
199
|
-
|
200
|
-
|
201
|
-
match = params[:message].match(@match) if @match && params[:message]
|
202
|
-
params.merge!(:match => match)
|
203
|
-
|
204
|
-
context = EventContext.new(params)
|
205
|
-
context.instance_eval(&@block)
|
206
|
-
context.commands
|
77
|
+
def topic(channel, text)
|
78
|
+
raw("TOPIC #{channel} :#{text}")
|
207
79
|
end
|
208
80
|
end
|
209
81
|
|
210
|
-
class
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
@
|
215
|
-
|
216
|
-
|
217
|
-
# Send a raw IRC message.
|
218
|
-
def raw(command)
|
219
|
-
@commands << command
|
82
|
+
class IRC
|
83
|
+
def initialize(bot, config)
|
84
|
+
@bot, @config = bot, config
|
85
|
+
@transfered = 0
|
86
|
+
@registration = []
|
87
|
+
@lock = false
|
88
|
+
@queue = []
|
220
89
|
end
|
221
90
|
|
222
|
-
|
223
|
-
|
224
|
-
|
91
|
+
def connect
|
92
|
+
@socket = TCPSocket.open(@config.server, @config.port)
|
93
|
+
message "PASSWORD #{@config.password}" if @config.password
|
94
|
+
message "NICK #{@config.nick}"
|
95
|
+
message "USER #{@config.nick} 0 * :#{@config.realname}"
|
96
|
+
@lock = true
|
97
|
+
|
98
|
+
# This should probably be somewhere else..
|
99
|
+
if @config.environment == :test
|
100
|
+
Thread.start {
|
101
|
+
while line = @socket.gets
|
102
|
+
parse line
|
103
|
+
end
|
104
|
+
}
|
105
|
+
else
|
106
|
+
while line = @socket.gets
|
107
|
+
parse line
|
108
|
+
end
|
109
|
+
end
|
225
110
|
end
|
226
111
|
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
# Join channel(s):
|
254
|
-
# join "#awesome_channel"
|
255
|
-
# join "#rollercoaster", "#j-lo"
|
256
|
-
def join(*channels)
|
257
|
-
channels.each {|channel| raw("JOIN #{channel}")}
|
112
|
+
def parse(input)
|
113
|
+
puts "<< #{input}" if @bot.config.verbose
|
114
|
+
case input
|
115
|
+
when /^:\S+ 00([1-4])/
|
116
|
+
@registration << $1.to_i
|
117
|
+
if registered?
|
118
|
+
@lock = false
|
119
|
+
@bot.dispatch(:connect)
|
120
|
+
continue_queue
|
121
|
+
end
|
122
|
+
when /^:(\S+)!\S+ PRIVMSG \S+ :?\001VERSION\001/
|
123
|
+
message "NOTICE #{$1} :\001VERSION #{@bot.config.version}\001"
|
124
|
+
when /^PING (\S+)/
|
125
|
+
@transfered, @lock = 0, false
|
126
|
+
message "PONG #{$1}"
|
127
|
+
when /^:(\S+)!(\S+) PRIVMSG (\S+) :?(.*)/
|
128
|
+
env = { :nick => $1, :userhost => $2, :channel => $3, :message => $4 }
|
129
|
+
type = env[:channel].match(/^#/) ? :channel : :private
|
130
|
+
@bot.dispatch(type, env)
|
131
|
+
when /^:\S+ ([4-5]\d\d) \S+ (\S+)/
|
132
|
+
env = {:error => $1.to_i, :message => $1, :nick => $2, :channel => $2}
|
133
|
+
@bot.dispatch(:error, env)
|
134
|
+
when /^:\S+ PONG/
|
135
|
+
@transfered, @lock = 0, false
|
136
|
+
continue_queue
|
137
|
+
end
|
258
138
|
end
|
259
139
|
|
260
|
-
|
261
|
-
|
262
|
-
# part "#rollercoaster", "#j-lo"
|
263
|
-
def part(*channels)
|
264
|
-
channels.each {|channel| raw("PART #{channel}")}
|
140
|
+
def registered?
|
141
|
+
([1,2,3,4] - @registration).empty?
|
265
142
|
end
|
266
143
|
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
raw("KICK #{channel} #{nick}#{comment}")
|
144
|
+
def message(msg)
|
145
|
+
@queue << msg
|
146
|
+
continue_queue
|
271
147
|
end
|
272
148
|
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
raw("NICK #{nickname}")
|
149
|
+
def continue_queue
|
150
|
+
# <= 1472 allows for \n
|
151
|
+
while !@lock && msg = @queue.shift
|
152
|
+
if (@transfered + msg.size) < 1472
|
153
|
+
@socket.puts msg
|
154
|
+
puts ">> #{msg}" if @bot.config.verbose
|
155
|
+
@transfered += msg.size + 1
|
156
|
+
else
|
157
|
+
@queue.unshift(msg)
|
158
|
+
@lock = true
|
159
|
+
@socket.puts "PING :#{@bot.config.server}"
|
160
|
+
break
|
161
|
+
end
|
162
|
+
end
|
288
163
|
end
|
289
164
|
end
|
290
165
|
end
|
291
166
|
|
292
|
-
|
293
|
-
%w(config helpers on).each do |method|
|
167
|
+
%w(configure helpers on).each do |method|
|
294
168
|
eval(<<-EOF)
|
295
169
|
def #{method}(*args, &block)
|
296
|
-
Isaac.
|
170
|
+
Isaac.bot.#{method}(*args, &block)
|
297
171
|
end
|
298
172
|
EOF
|
299
173
|
end
|
300
174
|
|
301
|
-
# Clever, thanks Sinatra.
|
302
175
|
at_exit do
|
303
|
-
|
304
|
-
|
176
|
+
unless defined?(Test::Unit)
|
177
|
+
raise $! if $!
|
178
|
+
Isaac.bot.start
|
179
|
+
end
|
305
180
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ichverstehe-isaac
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Harry Vangberg
|
@@ -9,12 +9,12 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2009-
|
12
|
+
date: 2009-02-23 00:00:00 -08:00
|
13
13
|
default_executable:
|
14
14
|
dependencies: []
|
15
15
|
|
16
16
|
description: Small DSL for writing IRC bots.
|
17
|
-
email:
|
17
|
+
email: harry@vangberg.name
|
18
18
|
executables: []
|
19
19
|
|
20
20
|
extensions: []
|