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.
Files changed (4) hide show
  1. data/README.rdoc +10 -16
  2. data/isaac.gemspec +3 -3
  3. data/lib/isaac.rb +123 -248
  4. 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
- config do |c|
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, /.*/ do
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 available for channel-events. +nick+ being the sender of the message, +match+ being a MatchData object returned by the regular expression you specified:
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[1]}' by #{nick}"
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[1]
42
- password = match[2]
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.0.4"
4
- s.date = "2009-01-28"
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 = "ichverstehe@gmail.com"
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
- # Returns the current instance of Isaac::Application
4
- def self.app
5
- @app ||= Application.new
6
- end
4
+ VERSION = '0.2.1'
5
+
6
+ Config = Struct.new(:server, :port, :password, :nick, :realname, :version, :environment, :verbose)
7
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)
8
+ def self.bot
9
+ @bot ||= Bot.new
14
10
  end
15
11
 
16
- Config = Struct.new(:nick, :server, :port, :username, :realname, :version, :verbose, :password)
12
+ class Bot
13
+ attr_accessor :config, :irc, :nick, :channel, :message, :userhost, :match,
14
+ :error
17
15
 
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
- @registration = []
23
- end
16
+ def initialize(&b)
17
+ @events = {}
18
+ @config = Config.new("localhost", 6667, nil, "isaac", "Isaac", 'isaac', :production, false)
24
19
 
25
- # This is plain stupid. Might be useful for logging or something later on.
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
- # Configure the bot:
33
- # config do |c|
34
- # c.server = "irc.freenode.net"
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
- # Methods defined inside the helpers-block will be available to on()-events at execution time.
49
- def helpers(&block)
50
- EventContext.class_eval(&block)
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
- # on()-events responds to certain actions. Depending on +type+ certain local variables are available:
54
- # +nick+, +channel+, +message+ and in particular +match+, which contains a MatchData object returned
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 execute(params={}, &block) #:nodoc:
79
- event = Event.new(:dsl, block)
80
- @queue << event.invoke(params)
37
+ def configure(&b)
38
+ b.call(@config)
81
39
  end
82
40
 
83
- def event(type, matcher)
84
- @events[type].detect do |e|
85
- type == :error ? matcher == e.match : matcher =~ e.match
86
- end
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
- @queue = Queue.new(@irc)
100
- @queue << @events[:connect].first.invoke if @events[:connect].first
46
+ event = @events[event] && @events[event].detect do |regexp,_|
47
+ message.match(regexp)
48
+ end
101
49
 
102
- while line = @irc.gets
103
- handle line
104
- end
105
- rescue Interrupt => e
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 registered?
114
- arr = [1,2,3,4] - @registration
115
- arr.empty?
57
+ def halt
58
+ throw :halt
116
59
  end
117
60
 
118
- # This is one hell of a nasty method. Something should be done, I suppose.
119
- def handle(line)
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
- # I luvz Rubyz
164
- def << (msg)
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
- # To prevent excess flood no more than 1472 bytes will be sent to the
170
- # server. When that limit is reached, @lock = true and the server will be
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
- class Event #:nodoc:
193
- attr_accessor :match, :block
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
- # Execute event in the context of EventContext.
200
- def invoke(params={})
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 EventContext
211
- attr_accessor :nick, :userhost, :channel, :message, :match, :commands
212
- def initialize(args = {})
213
- args.each {|k,v| instance_variable_set("@#{k}",v)}
214
- @commands = []
215
- end
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
- # Send a message to nick/channel.
223
- def msg(recipient, text)
224
- raw("PRIVMSG #{recipient} :#{text}")
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
- # Send a notice to nick/channel
228
- def notice(recipient, text)
229
- raw("PRIVMSG #{recipient} :#{text}")
230
- end
231
-
232
- # Set modes(s)
233
- # mode "#awesome" "+im"
234
- # mode "arnie" "+o-v"
235
- # mode "#awesome" "+k" "password"
236
- def mode(target, mode, option=nil)
237
- option = " #{option}" if option
238
- raw "MODE #{target} #{mode}#{option}"
239
- end
240
-
241
- # Ban a hostmask
242
- # ban_mask "#awesome" "*!*@*"
243
- def ban_mask(channel, mask)
244
- mode channel, "+b", mask
245
- end
246
-
247
- # Unban a hostmask
248
- # unban_mask "#awesome" "*!*@*"
249
- def unban_mask(channel, mask)
250
- mode channel, "-b", mask
251
- end
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
- # Part channel(s):
261
- # part "#awesome_channel"
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
- # Kick nick from channel, with optional comment.
268
- def kick(channel, nick, comment=nil)
269
- comment = " :#{comment}" if comment
270
- raw("KICK #{channel} #{nick}#{comment}")
144
+ def message(msg)
145
+ @queue << msg
146
+ continue_queue
271
147
  end
272
148
 
273
- # Change topic of channel.
274
- def topic(channel, topic)
275
- raw("TOPIC #{channel} :#{topic}")
276
- end
277
-
278
- # Invite nicks to channel
279
- # invite "#awesome_channel", "arnie"
280
- # invite "#awesome_channel", "arnie", "brigitte"
281
- def invite(channel, *nicks)
282
- nicks.each {|nick| raw("INVITE #{nick} #{channel}")}
283
- end
284
-
285
- # Change nickname
286
- def set_nick(nickname)
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
- # Assign methods to current Isaac instance
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.app.#{method}(*args, &block)
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
- raise $! if $!
304
- Isaac.app.start
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.0.4
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-01-28 00:00:00 -08:00
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: ichverstehe@gmail.com
17
+ email: harry@vangberg.name
18
18
  executables: []
19
19
 
20
20
  extensions: []