isaac 0.0.2
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 +18 -0
- data/lib/isaac.rb +245 -0
- metadata +58 -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,18 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = "isaac"
|
3
|
+
s.version = "0.0.2"
|
4
|
+
s.date = "2008-11-15"
|
5
|
+
s.summary = "The smallish DSL for writing IRC bots"
|
6
|
+
s.email = "ichverstehe@gmail.com"
|
7
|
+
s.homepage = "http://github.com/ichverstehe/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"]
|
12
|
+
s.files = ["README.rdoc",
|
13
|
+
"LICENSE",
|
14
|
+
"isaac.gemspec",
|
15
|
+
"lib/isaac.rb"]
|
16
|
+
s.rdoc_options = ["--main", "README.rdoc"]
|
17
|
+
s.extra_rdoc_files = ["LICENSE", "README.rdoc"]
|
18
|
+
end
|
data/lib/isaac.rb
ADDED
@@ -0,0 +1,245 @@
|
|
1
|
+
require 'socket'
|
2
|
+
module Isaac
|
3
|
+
# Returns the current instance of Isaac::Application
|
4
|
+
def self.app
|
5
|
+
@app ||= Application.new
|
6
|
+
end
|
7
|
+
|
8
|
+
# Use EventContext methods such as msg(), join() etc. outside on()-events. See +examples/execute.rb+.
|
9
|
+
# Isaac.execute do
|
10
|
+
# msg 'harryjr', 'you're awesome'
|
11
|
+
# end
|
12
|
+
def self.execute(params={}, &block)
|
13
|
+
app.execute(params, &block)
|
14
|
+
end
|
15
|
+
|
16
|
+
Config = Struct.new(:nick, :server, :port, :username, :realname, :verbose)
|
17
|
+
|
18
|
+
# These are top level methods you use to construct your bot.
|
19
|
+
class Application
|
20
|
+
def initialize #:nodoc:
|
21
|
+
@events = Hash.new {|k,v| k[v] = []}
|
22
|
+
end
|
23
|
+
|
24
|
+
# This is plain stupid. Might be useful for logging or something later on.
|
25
|
+
def start #:nodoc:
|
26
|
+
puts " ==== Starting Isaac ==== "
|
27
|
+
loop { connect }
|
28
|
+
puts " ==== Ending Isaac ==== "
|
29
|
+
end
|
30
|
+
|
31
|
+
# Configure the bot:
|
32
|
+
# config do |c|
|
33
|
+
# c.server = "irc.freenode.net"
|
34
|
+
# c.nick = "AwesomeBot"
|
35
|
+
# c.port = 6667
|
36
|
+
# c.realname = "James Dean"
|
37
|
+
# c.username = "jdean"
|
38
|
+
# c.verbose = true
|
39
|
+
# end
|
40
|
+
def config(&block)
|
41
|
+
@config = Config.new('isaac_bot', 'irc.freenode.net', 6667, 'isaac', 'isaac', false)
|
42
|
+
block.call(@config)
|
43
|
+
@config
|
44
|
+
end
|
45
|
+
|
46
|
+
# Methods defined inside the helpers-block will be available to on()-events at execution time.
|
47
|
+
def helpers(&block)
|
48
|
+
EventContext.class_eval(&block)
|
49
|
+
end
|
50
|
+
|
51
|
+
# on()-events responds to certain actions. Depending on +type+ certain local variables are available: +nick+, +channel+, +message+ and in particular +match+, which contains a MatchData object returned by the given regular expression.
|
52
|
+
#
|
53
|
+
# * Do something after connection has been established, e.g. join channels.
|
54
|
+
# on :connect do
|
55
|
+
# join "#awesome_channel", "#lee_marvin_fans"
|
56
|
+
# end
|
57
|
+
# * Respond to private messages matching a given regular expression.
|
58
|
+
# on :private, /^echo (.*)/ do
|
59
|
+
# msg nick, "You said '#{match[1]}!"
|
60
|
+
# end
|
61
|
+
# * Respond to messages matching a given regular expression send to a channel.
|
62
|
+
# on :channel, /quote/ do
|
63
|
+
# msg channel, "#{nick} requested a quote: 'Smoking, a subtle form a suicide.' - Vonnegut"
|
64
|
+
# end
|
65
|
+
# * Respond to error codes, according to the RFC.
|
66
|
+
# on :error, 401 do
|
67
|
+
# # Execute this if you try to send a message to a non-existing nick/channel.
|
68
|
+
# end
|
69
|
+
def on(type, match=nil, &block)
|
70
|
+
@events[type] << e = Event.new(match, block)
|
71
|
+
return e
|
72
|
+
end
|
73
|
+
|
74
|
+
def execute(params={}, &block) #:nodoc:
|
75
|
+
event = Event.new(:dsl, block)
|
76
|
+
@queue << event.invoke(params)
|
77
|
+
end
|
78
|
+
|
79
|
+
def event(type, matcher)
|
80
|
+
@events[type].detect do |e|
|
81
|
+
type == :error ? matcher == e.match : matcher =~ e.match
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def connect
|
86
|
+
begin
|
87
|
+
puts "Connecting to #{@config.server} at port #{@config.port}"
|
88
|
+
@irc = TCPSocket.open(@config.server, @config.port)
|
89
|
+
puts "Connection established."
|
90
|
+
|
91
|
+
@queue = Queue.new(@irc)
|
92
|
+
@queue << "NICK #{@config.nick}"
|
93
|
+
@queue << "USER #{@config.username} foobar foobar :#{@config.realname}"
|
94
|
+
@queue << @events[:connect].first.invoke if @events[:connect].first
|
95
|
+
|
96
|
+
while line = @irc.gets
|
97
|
+
handle line
|
98
|
+
end
|
99
|
+
rescue Interrupt => e
|
100
|
+
puts "Disconnected! An error occurred: #{e.inspect}"
|
101
|
+
rescue Timeout::Error => e
|
102
|
+
puts "Timeout: #{e}"
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# This is one hell of a nasty method. Something should be done, I suppose.
|
107
|
+
def handle(line)
|
108
|
+
puts "< #{line}" if @config.verbose
|
109
|
+
|
110
|
+
case line
|
111
|
+
when /^:(\S+)!(\S+) PRIVMSG (\S+) :?(.*)/
|
112
|
+
nick, userhost, channel, message = $1, $2, $3, $4
|
113
|
+
type = channel.match(/^#/) ? :channel : :private
|
114
|
+
if event = event(type, message)
|
115
|
+
@queue << event.invoke(:nick => nick, :userhost => userhost, :channel => channel, :message => message)
|
116
|
+
end
|
117
|
+
when /^:\S+ ([4-5]\d\d) \S+ (\S+)/
|
118
|
+
error = $1
|
119
|
+
nick = channel = $2
|
120
|
+
if event = event(:error, error.to_i)
|
121
|
+
@queue << event.invoke(:nick => nick, :channel => channel)
|
122
|
+
end
|
123
|
+
when /^PING (\S+)/
|
124
|
+
#TODO not sure this is correct. Damned RFC.
|
125
|
+
@queue << "PONG #{$1}"
|
126
|
+
when /^:\S+ PONG \S+ :excess/
|
127
|
+
@queue.lock = false
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
class Queue #:nodoc:
|
133
|
+
attr_accessor :lock
|
134
|
+
def initialize(socket)
|
135
|
+
@socket = socket
|
136
|
+
@queue = []
|
137
|
+
@transfered = 0
|
138
|
+
@lock = false
|
139
|
+
transmit
|
140
|
+
end
|
141
|
+
|
142
|
+
# I luvz Rubyz
|
143
|
+
def << (msg)
|
144
|
+
# .flatten! returns nill if no modifications were made, thus we do this.
|
145
|
+
@queue = (@queue << msg).flatten
|
146
|
+
end
|
147
|
+
|
148
|
+
# To prevent excess flood no more than 1472 bytes will be sent to the
|
149
|
+
# server. When that limit is reached, @lock = true and the server will be
|
150
|
+
# PINGed. @lock will be true until a PONG is received (Application#handle).
|
151
|
+
def transmit
|
152
|
+
Thread.start { loop {
|
153
|
+
unless @lock || @queue.empty?
|
154
|
+
msg = @queue.shift
|
155
|
+
if (@transfered + msg.size) > 1472
|
156
|
+
# No honestly, :excess. The RFC is not too clear on this subject TODO
|
157
|
+
@socket.puts "PING :excess"
|
158
|
+
@lock = true
|
159
|
+
@transfered = 0
|
160
|
+
else
|
161
|
+
@socket.puts msg
|
162
|
+
@transfered += msg.size
|
163
|
+
end
|
164
|
+
end
|
165
|
+
sleep 0.1
|
166
|
+
}}
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
class Event #:nodoc:
|
171
|
+
attr_accessor :match, :block
|
172
|
+
def initialize(match, block)
|
173
|
+
@match = match
|
174
|
+
@block = block
|
175
|
+
end
|
176
|
+
|
177
|
+
# Execute event in the context of EventContext.
|
178
|
+
def invoke(params={})
|
179
|
+
match = params[:message].match(@match) if @match && params[:message]
|
180
|
+
params.merge!(:match => match)
|
181
|
+
|
182
|
+
context = EventContext.new(params)
|
183
|
+
context.instance_eval(&@block)
|
184
|
+
context.commands
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
class EventContext
|
189
|
+
attr_accessor :nick, :userhost, :channel, :message, :match, :commands
|
190
|
+
def initialize(args = {})
|
191
|
+
args.each {|k,v| instance_variable_set("@#{k}",v)}
|
192
|
+
@commands = []
|
193
|
+
end
|
194
|
+
|
195
|
+
# Send a raw IRC message.
|
196
|
+
def raw(command)
|
197
|
+
@commands << command
|
198
|
+
end
|
199
|
+
|
200
|
+
# Send a message to nick/channel.
|
201
|
+
def msg(recipient, text)
|
202
|
+
raw("PRIVMSG #{recipient} :#{text}")
|
203
|
+
end
|
204
|
+
|
205
|
+
# Join channel(s):
|
206
|
+
# join "#awesome_channel"
|
207
|
+
# join "#rollercoaster", "#j-lo"
|
208
|
+
def join(*channels)
|
209
|
+
channels.each {|channel| raw("JOIN #{channel}")}
|
210
|
+
end
|
211
|
+
|
212
|
+
# Part channel(s):
|
213
|
+
# part "#awesome_channel"
|
214
|
+
# part "#rollercoaster", "#j-lo"
|
215
|
+
def part(*channels)
|
216
|
+
channels.each {|channel| raw("PART #{channel}")}
|
217
|
+
end
|
218
|
+
|
219
|
+
# Kick nick from channel, with optional comment.
|
220
|
+
def kick(channel, nick, comment=nil)
|
221
|
+
comment = " :#{comment}" if comment
|
222
|
+
raw("KICK #{channel} #{nick}#{comment}")
|
223
|
+
end
|
224
|
+
|
225
|
+
# Change topic of channel.
|
226
|
+
def topic(channel, topic)
|
227
|
+
raw("TOPIC #{channel} :#{topic}")
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
# Assign methods to current Isaac instance
|
233
|
+
%w(config helpers on).each do |method|
|
234
|
+
eval(<<-EOF)
|
235
|
+
def #{method}(*args, &block)
|
236
|
+
Isaac.app.#{method}(*args, &block)
|
237
|
+
end
|
238
|
+
EOF
|
239
|
+
end
|
240
|
+
|
241
|
+
# Clever, thanks Sinatra.
|
242
|
+
at_exit do
|
243
|
+
raise $! if $!
|
244
|
+
Isaac.app.start
|
245
|
+
end
|
metadata
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: isaac
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Harry Vangberg
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2008-11-15 00:00:00 +01:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: Small DSL for writing IRC bots.
|
17
|
+
email: ichverstehe@gmail.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files:
|
23
|
+
- LICENSE
|
24
|
+
- README.rdoc
|
25
|
+
files:
|
26
|
+
- README.rdoc
|
27
|
+
- LICENSE
|
28
|
+
- isaac.gemspec
|
29
|
+
- lib/isaac.rb
|
30
|
+
has_rdoc: true
|
31
|
+
homepage: http://github.com/ichverstehe/isaac
|
32
|
+
post_install_message:
|
33
|
+
rdoc_options:
|
34
|
+
- --main
|
35
|
+
- README.rdoc
|
36
|
+
require_paths:
|
37
|
+
- lib
|
38
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
39
|
+
requirements:
|
40
|
+
- - ">="
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: "0"
|
43
|
+
version:
|
44
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - ">="
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: "0"
|
49
|
+
version:
|
50
|
+
requirements: []
|
51
|
+
|
52
|
+
rubyforge_project: isaac
|
53
|
+
rubygems_version: 1.2.0
|
54
|
+
signing_key:
|
55
|
+
specification_version: 2
|
56
|
+
summary: The smallish DSL for writing IRC bots
|
57
|
+
test_files: []
|
58
|
+
|