ichverstehe-isaac 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (5) hide show
  1. data/LICENSE +6 -0
  2. data/README.rdoc +94 -0
  3. data/isaac.gemspec +18 -0
  4. data/lib/isaac.rb +257 -0
  5. 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-17"
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,257 @@
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, :version, :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
+ 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.version = "James Dean Bot v2.34"
39
+ # c.verbose = true
40
+ # end
41
+ def config(&block)
42
+ @config = Config.new('isaac_bot', 'irc.freenode.net', 6667, 'isaac', 'isaac', 'isaac-bot', false)
43
+ block.call(@config)
44
+ @config
45
+ end
46
+
47
+ # Methods defined inside the helpers-block will be available to on()-events at execution time.
48
+ def helpers(&block)
49
+ EventContext.class_eval(&block)
50
+ end
51
+
52
+ # on()-events responds to certain actions. Depending on +type+ certain local variables are available:
53
+ # +nick+, +channel+, +message+ and in particular +match+, which contains a MatchData object returned
54
+ # by the given regular expression.
55
+ #
56
+ # * Do something after connection has been established, e.g. join channels.
57
+ # on :connect do
58
+ # join "#awesome_channel", "#lee_marvin_fans"
59
+ # end
60
+ # * Respond to private messages matching a given regular expression.
61
+ # on :private, /^echo (.*)/ do
62
+ # msg nick, "You said '#{match[1]}!"
63
+ # end
64
+ # * Respond to messages matching a given regular expression send to a channel.
65
+ # on :channel, /quote/ do
66
+ # msg channel, "#{nick} requested a quote: 'Smoking, a subtle form a suicide.' - Vonnegut"
67
+ # end
68
+ # * Respond to error codes, according to the RFC.
69
+ # on :error, 401 do
70
+ # # Execute this if you try to send a message to a non-existing nick/channel.
71
+ # end
72
+ def on(type, match=nil, &block)
73
+ @events[type] << e = Event.new(match, block)
74
+ return e
75
+ end
76
+
77
+ def execute(params={}, &block) #:nodoc:
78
+ event = Event.new(:dsl, block)
79
+ @queue << event.invoke(params)
80
+ end
81
+
82
+ def event(type, matcher)
83
+ @events[type].detect do |e|
84
+ type == :error ? matcher == e.match : matcher =~ e.match
85
+ end
86
+ end
87
+
88
+ def connect
89
+ begin
90
+ puts "Connecting to #{@config.server} at port #{@config.port}"
91
+ @irc = TCPSocket.open(@config.server, @config.port)
92
+ puts "Connection established."
93
+
94
+ @queue = Queue.new(@irc)
95
+ @queue << "NICK #{@config.nick}"
96
+ @queue << "USER #{@config.username} foobar foobar :#{@config.realname}"
97
+ @queue << @events[:connect].first.invoke if @events[:connect].first
98
+
99
+ while line = @irc.gets
100
+ handle line
101
+ end
102
+ rescue Interrupt => e
103
+ puts "Disconnected! An error occurred: #{e.inspect}"
104
+ rescue Timeout::Error => e
105
+ puts "Timeout: #{e}. Reconnecting."
106
+ connect
107
+ end
108
+ end
109
+
110
+ # This is one hell of a nasty method. Something should be done, I suppose.
111
+ def handle(line)
112
+ puts "> #{line}" if @config.verbose
113
+
114
+ case line
115
+ when /^:(\S+)!\S+ PRIVMSG \S+ :?\001VERSION\001/
116
+ @queue << "NOTICE #{$1} :\001VERSION #{@config.version}\001"
117
+ when /^:(\S+)!(\S+) PRIVMSG (\S+) :?(.*)/
118
+ nick, userhost, channel, message = $1, $2, $3, $4
119
+ type = channel.match(/^#/) ? :channel : :private
120
+ if event = event(type, message)
121
+ @queue << event.invoke(:nick => nick, :userhost => userhost, :channel => channel, :message => message)
122
+ end
123
+ when /^:\S+ ([4-5]\d\d) \S+ (\S+)/
124
+ error = $1
125
+ nick = channel = $2
126
+ if event = event(:error, error.to_i)
127
+ @queue << event.invoke(:nick => nick, :channel => channel)
128
+ end
129
+ when /^PING (\S+)/
130
+ #TODO not sure this is correct. Damned RFC.
131
+ @queue << "PONG #{$1}"
132
+ when /^:\S+ PONG \S+ :excess/
133
+ @queue.lock = false
134
+ end
135
+ end
136
+ end
137
+
138
+ class Queue #:nodoc:
139
+ attr_accessor :lock
140
+ def initialize(socket)
141
+ @socket = socket
142
+ @queue = []
143
+ @transfered = 0
144
+ @lock = false
145
+ transmit
146
+ end
147
+
148
+ # I luvz Rubyz
149
+ def << (msg)
150
+ # .flatten! returns nill if no modifications were made, thus we do this.
151
+ @queue = (@queue << msg).flatten
152
+ end
153
+
154
+ # To prevent excess flood no more than 1472 bytes will be sent to the
155
+ # server. When that limit is reached, @lock = true and the server will be
156
+ # PINGed. @lock will be true until a PONG is received (Application#handle).
157
+ def transmit
158
+ Thread.start { loop {
159
+ unless @lock || @queue.empty?
160
+ msg = @queue.first
161
+ if (@transfered + msg.size) > 1472
162
+ # No honestly, :excess. The RFC is not too clear on this subject TODO
163
+ @socket.puts "PING :excess"
164
+ @lock = true
165
+ @transfered = 0
166
+ else
167
+ @socket.puts msg
168
+ @transfered += msg.size
169
+ @queue.shift
170
+ end
171
+ end
172
+ sleep 0.1
173
+ }}
174
+ end
175
+ end
176
+
177
+ class Event #:nodoc:
178
+ attr_accessor :match, :block
179
+ def initialize(match, block)
180
+ @match = match
181
+ @block = block
182
+ end
183
+
184
+ # Execute event in the context of EventContext.
185
+ def invoke(params={})
186
+ match = params[:message].match(@match) if @match && params[:message]
187
+ params.merge!(:match => match)
188
+
189
+ context = EventContext.new(params)
190
+ context.instance_eval(&@block)
191
+ context.commands
192
+ end
193
+ end
194
+
195
+ class EventContext
196
+ attr_accessor :nick, :userhost, :channel, :message, :match, :commands
197
+ def initialize(args = {})
198
+ args.each {|k,v| instance_variable_set("@#{k}",v)}
199
+ @commands = []
200
+ end
201
+
202
+ # Send a raw IRC message.
203
+ def raw(command)
204
+ @commands << command
205
+ end
206
+
207
+ # Send a message to nick/channel.
208
+ def msg(recipient, text)
209
+ raw("PRIVMSG #{recipient} :#{text}")
210
+ end
211
+
212
+ # Send a notice to nick/channel
213
+ def notice(recipient, text)
214
+ raw("PRIVMSG #{recipient} :#{text}")
215
+ end
216
+
217
+ # Join channel(s):
218
+ # join "#awesome_channel"
219
+ # join "#rollercoaster", "#j-lo"
220
+ def join(*channels)
221
+ channels.each {|channel| raw("JOIN #{channel}")}
222
+ end
223
+
224
+ # Part channel(s):
225
+ # part "#awesome_channel"
226
+ # part "#rollercoaster", "#j-lo"
227
+ def part(*channels)
228
+ channels.each {|channel| raw("PART #{channel}")}
229
+ end
230
+
231
+ # Kick nick from channel, with optional comment.
232
+ def kick(channel, nick, comment=nil)
233
+ comment = " :#{comment}" if comment
234
+ raw("KICK #{channel} #{nick}#{comment}")
235
+ end
236
+
237
+ # Change topic of channel.
238
+ def topic(channel, topic)
239
+ raw("TOPIC #{channel} :#{topic}")
240
+ end
241
+ end
242
+ end
243
+
244
+ # Assign methods to current Isaac instance
245
+ %w(config helpers on).each do |method|
246
+ eval(<<-EOF)
247
+ def #{method}(*args, &block)
248
+ Isaac.app.#{method}(*args, &block)
249
+ end
250
+ EOF
251
+ end
252
+
253
+ # Clever, thanks Sinatra.
254
+ at_exit do
255
+ raise $! if $!
256
+ Isaac.app.start
257
+ end
metadata ADDED
@@ -0,0 +1,58 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ichverstehe-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-17 00:00:00 -08: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
+