isaac 0.2.5 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (7) hide show
  1. data/CHANGES +43 -0
  2. data/README.rdoc +41 -1
  3. data/isaac.gemspec +9 -6
  4. data/lib/isaac.rb +4 -210
  5. data/lib/isaac/bot.rb +338 -0
  6. metadata +5 -4
  7. data/LICENSE +0 -6
data/CHANGES ADDED
@@ -0,0 +1,43 @@
1
+ = master / unreleasedd
2
+
3
+ * Require 'isaac/bot' to avoid top level bot/methods [Julian Langschaedel]
4
+
5
+ * Add action(), kick() and mode() [Danny Tatom]
6
+
7
+ * SSL support - configure {|c| c.ssl = true} [postmodern]
8
+
9
+ * on :join/:part-events [postmodern]
10
+
11
+ = 0.2.5 / 2009-04-25
12
+
13
+ * Comply to the RFC - lines end with \r\n, not just \n.
14
+
15
+ = 0.2.4 / 2009-04-01
16
+
17
+ * Bug fixes.
18
+
19
+ = 0.2.3 / 2009-04-01
20
+
21
+ * Pass regular expression groups to block parameters.
22
+
23
+ * Internal refactoring of queue and tests.
24
+
25
+ = 0.2.2 / 2009-02-23
26
+
27
+ * Irrelevant.
28
+
29
+ = 0.2.1 / 2009-02-23
30
+
31
+ * config() has been renamed to configure().
32
+
33
+ * part() and topic() commands added.
34
+
35
+ * Comply to the RFC and wait for 001-004 messages until sending commands.
36
+
37
+ * Respond to CTCP action with the content of configure {|c| c.version = "" }.
38
+
39
+ * Improved documentation.
40
+
41
+ = 0.2.0 / 2009-02-23
42
+
43
+ * This is a complete rewrite of Isaac.
@@ -74,8 +74,48 @@ Errors, as specified by RFC 1459, can be reacted upon as well. If you e.g. try t
74
74
 
75
75
  Available variables: +nick+ and +channel+.
76
76
 
77
+ === Non-top level bots
78
+ You might not want to pollute the top-level namespace with Isaac
79
+ helpers, or you want to define multiple bots. This can be done
80
+ easily, by requiring `isaac/bot` instead of `isaac`:
81
+
82
+ require 'isaac/bot'
83
+
84
+ bot = Isaac::Bot.new do
85
+ configure do
86
+
87
+ end
88
+
89
+ on :channel do
90
+
91
+ end
92
+ end
93
+
94
+ bot.start
95
+
77
96
  == Contribute
78
97
  The source is hosted at GitHub: http://github.com/ichverstehe/isaac
79
98
 
80
99
  == License
81
- The MIT. Google it.
100
+ Copyright (c) 2009 Harry Vangberg <harry@vangberg.name>
101
+
102
+ Permission is hereby granted, free of charge, to any person
103
+ obtaining a copy of this software and associated documentation
104
+ files (the "Software"), to deal in the Software without
105
+ restriction, including without limitation the rights to use,
106
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
107
+ copies of the Software, and to permit persons to whom the
108
+ Software is furnished to do so, subject to the following
109
+ conditions:
110
+
111
+ The above copyright notice and this permission notice shall be
112
+ included in all copies or substantial portions of the Software.
113
+
114
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
115
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
116
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
117
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
118
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
119
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
120
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
121
+ OTHER DEALINGS IN THE SOFTWARE.
@@ -1,7 +1,7 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = "isaac"
3
- s.version = "0.2.5"
4
- s.date = "2009-04-25"
3
+ s.version = "0.2.6"
4
+ s.date = "2009-12-21"
5
5
  s.summary = "The smallish DSL for writing IRC bots"
6
6
  s.email = "harry@vangberg.name"
7
7
  s.homepage = "http://github.com/ichverstehe/isaac"
@@ -9,11 +9,14 @@ Gem::Specification.new do |s|
9
9
  s.rubyforge_project = "isaac"
10
10
  s.has_rdoc = true
11
11
  s.authors = ["Harry Vangberg"]
12
- s.files = ["README.rdoc",
13
- "LICENSE",
12
+ s.files = [
13
+ "README.rdoc",
14
+ "CHANGES",
14
15
  "isaac.gemspec",
15
- "lib/isaac.rb"]
16
+ "lib/isaac.rb",
17
+ "lib/isaac/bot.rb"
18
+ ]
16
19
  s.rdoc_options = ["--main", "README.rdoc"]
17
- s.extra_rdoc_files = ["LICENSE", "README.rdoc"]
20
+ s.extra_rdoc_files = ["CHANGES", "README.rdoc"]
18
21
  end
19
22
 
@@ -1,217 +1,11 @@
1
- require 'socket'
1
+ require 'isaac/bot'
2
2
 
3
- module Isaac
4
- VERSION = '0.2.1'
5
-
6
- Config = Struct.new(:server, :port, :password, :nick, :realname, :version, :environment, :verbose)
7
-
8
- def self.bot
9
- @bot ||= Bot.new
10
- end
11
-
12
- class Bot
13
- attr_accessor :config, :irc, :nick, :channel, :message, :userhost, :match,
14
- :error
15
-
16
- def initialize(&b)
17
- @events = {}
18
- @config = Config.new("localhost", 6667, nil, "isaac", "Isaac", 'isaac', :production, false)
19
-
20
- instance_eval(&b) if block_given?
21
- end
22
-
23
- def configure(&b)
24
- b.call(@config)
25
- end
26
-
27
- def on(event, match=//, &block)
28
- match = match.to_s if match.is_a? Integer
29
- (@events[event] ||= []) << [Regexp.new(match), block]
30
- end
31
-
32
- def helpers(&b)
33
- instance_eval(&b)
34
- end
35
-
36
- def halt
37
- throw :halt
38
- end
39
-
40
- def raw(command)
41
- @irc.message(command)
42
- end
43
-
44
- def msg(recipient, text)
45
- raw("PRIVMSG #{recipient} :#{text}")
46
- end
47
-
48
- def join(*channels)
49
- channels.each {|channel| raw("JOIN #{channel}")}
50
- end
51
-
52
- def part(*channels)
53
- channels.each {|channel| raw("PART #{channel}")}
54
- end
55
-
56
- def topic(channel, text)
57
- raw("TOPIC #{channel} :#{text}")
58
- end
59
-
60
- def start
61
- puts "Connecting to #{@config.server}:#{@config.port}" unless @config.environment == :test
62
- @irc = IRC.new(self, @config)
63
- @irc.connect
64
- end
65
-
66
- def dispatch(event, env={})
67
- self.nick, self.userhost, self.channel, self.error =
68
- env[:nick], env[:userhost], env[:channel], env[:error]
69
- self.message = env[:message] || ""
70
-
71
- if handler = find(event, message)
72
- regexp, block = *handler
73
- self.match = message.match(regexp).captures
74
- invoke block
75
- end
76
- end
77
-
78
- private
79
- def find(type, message)
80
- if events = @events[type]
81
- events.detect {|regexp,_| message.match(regexp)}
82
- end
83
- end
84
-
85
- def invoke(block)
86
- mc = class << self; self; end
87
- mc.send :define_method, :__isaac_event_handler, &block
88
-
89
- bargs = case block.arity <=> 0
90
- when -1; match
91
- when 0; []
92
- when 1; match[0..block.arity-1]
93
- end
94
-
95
- catch(:halt) { __isaac_event_handler(*bargs) }
96
- end
97
- end
98
-
99
- class IRC
100
- def initialize(bot, config)
101
- @bot, @config = bot, config
102
- @transfered = 0
103
- @registration = []
104
- end
105
-
106
- def connect
107
- @socket = TCPSocket.open(@config.server, @config.port)
108
- @queue = Queue.new(@socket, @bot.config.server)
109
- message "PASS #{@config.password}" if @config.password
110
- message "NICK #{@config.nick}"
111
- message "USER #{@config.nick} 0 * :#{@config.realname}"
112
- @queue.lock
113
-
114
- while line = @socket.gets
115
- parse line
116
- end
117
- end
118
-
119
- def parse(input)
120
- puts "<< #{input}" if @bot.config.verbose
121
- case input.chomp
122
- when /(^:\S+ )?00([1-4])/
123
- @registration << $2.to_i
124
- if registered?
125
- @queue.unlock
126
- @bot.dispatch(:connect)
127
- end
128
- when /(^:(\S+)!\S+ )?PRIVMSG \S+ :?\001VERSION\001/
129
- message "NOTICE #{$2} :\001VERSION #{@bot.config.version}\001"
130
- when /^PING (\S+)/
131
- @queue.unlock
132
- message "PONG #{$1}"
133
- when /(^:(\S+)!(\S+) )?PRIVMSG (\S+) :?(.*)/
134
- env = { :nick => $2, :userhost => $3, :channel => $4, :message => $5 }
135
- type = env[:channel].match(/^#/) ? :channel : :private
136
- @bot.dispatch(type, env)
137
- when /(^:\S+ )?([4-5]\d\d) \S+ (\S+)/
138
- env = {:error => $2.to_i, :message => $2, :nick => $3, :channel => $3}
139
- @bot.dispatch(:error, env)
140
- when /(^:\S+ )?PONG/
141
- @queue.unlock
142
- end
143
- end
144
-
145
- def registered?
146
- ([1,2,3,4] - @registration).empty?
147
- end
148
-
149
- def message(msg)
150
- @queue << msg
151
- end
152
- end
153
-
154
- class Queue
155
- def initialize(socket, server)
156
- # We need server for pinging us out of an excess flood
157
- @socket, @server = socket, server
158
- @queue, @lock, @transfered = [], false, 0
159
- end
160
-
161
- def lock
162
- @lock = true
163
- end
164
-
165
- def unlock
166
- @lock, @transfered = false, 0
167
- invoke
168
- end
169
-
170
- def <<(message)
171
- @queue << message
172
- invoke
173
- end
174
-
175
- private
176
- def message_to_send?
177
- !@lock && !@queue.empty?
178
- end
179
-
180
- def transfered_after_next_send
181
- @transfered + @queue.first.size + 2 # the 2 is for \r\n
182
- end
183
-
184
- def exceed_limit?
185
- transfered_after_next_send > 1472
186
- end
187
-
188
- def lock_and_ping
189
- lock
190
- @socket.print "PING :#{@server}\r\n"
191
- end
192
-
193
- def next_message
194
- @queue.shift.to_s.chomp + "\r\n"
195
- end
196
-
197
- def invoke
198
- while message_to_send?
199
- if exceed_limit?
200
- lock_and_ping; break
201
- else
202
- @transfered = transfered_after_next_send
203
- @socket.print next_message
204
- # puts ">> #{msg}" if @bot.config.verbose
205
- end
206
- end
207
- end
208
- end
209
- end
3
+ $bot = Isaac::Bot.new
210
4
 
211
5
  %w(configure helpers on).each do |method|
212
6
  eval(<<-EOF)
213
7
  def #{method}(*args, &block)
214
- Isaac.bot.#{method}(*args, &block)
8
+ $bot.#{method}(*args, &block)
215
9
  end
216
10
  EOF
217
11
  end
@@ -219,6 +13,6 @@ end
219
13
  at_exit do
220
14
  unless defined?(Test::Unit)
221
15
  raise $! if $!
222
- Isaac.bot.start
16
+ $bot.start
223
17
  end
224
18
  end
@@ -0,0 +1,338 @@
1
+ require 'socket'
2
+
3
+ module Isaac
4
+ VERSION = '0.2.1'
5
+
6
+ Config = Struct.new(:server, :port, :ssl, :password, :nick, :realname, :version, :environment, :verbose)
7
+
8
+ class Bot
9
+ attr_accessor :config, :irc, :nick, :channel, :message, :user, :host, :match,
10
+ :error
11
+
12
+ def initialize(&b)
13
+ @events = {}
14
+ @config = Config.new("localhost", 6667, false, nil, "isaac", "Isaac", 'isaac', :production, false)
15
+
16
+ instance_eval(&b) if block_given?
17
+ end
18
+
19
+ def configure(&b)
20
+ b.call(@config)
21
+ end
22
+
23
+ def on(event, match=//, &block)
24
+ match = match.to_s if match.is_a? Integer
25
+ (@events[event] ||= []) << [Regexp.new(match), block]
26
+ end
27
+
28
+ def helpers(&b)
29
+ instance_eval(&b)
30
+ end
31
+
32
+ def halt
33
+ throw :halt
34
+ end
35
+
36
+ def raw(command)
37
+ @irc.message(command)
38
+ end
39
+
40
+ def msg(recipient, text)
41
+ raw("PRIVMSG #{recipient} :#{text}")
42
+ end
43
+
44
+ def action(recipient, text)
45
+ raw("PRIVMSG #{recipient} :\001ACTION #{text}\001")
46
+ end
47
+
48
+ def join(*channels)
49
+ channels.each {|channel| raw("JOIN #{channel}")}
50
+ end
51
+
52
+ def part(*channels)
53
+ channels.each {|channel| raw("PART #{channel}")}
54
+ end
55
+
56
+ def topic(channel, text)
57
+ raw("TOPIC #{channel} :#{text}")
58
+ end
59
+
60
+ def mode(channel, option)
61
+ raw("MODE #{channel} #{option}")
62
+ end
63
+
64
+ def kick(channel, user, reason=nil)
65
+ raw("KICK #{channel} #{user} :#{reason}")
66
+ end
67
+
68
+ def quit(message=nil)
69
+ command = message ? "QUIT :#{message}" : "QUIT"
70
+ raw command
71
+ end
72
+
73
+ def start
74
+ puts "Connecting to #{@config.server}:#{@config.port}" unless @config.environment == :test
75
+ @irc = IRC.new(self, @config)
76
+ @irc.connect
77
+ end
78
+
79
+ def message
80
+ @message ||= ""
81
+ end
82
+
83
+ def dispatch(event, msg=nil)
84
+ if msg
85
+ @nick, @user, @host, @channel, @error, @message =
86
+ msg.nick, msg.user, msg.host, msg.channel, msg.error, msg.message
87
+ end
88
+
89
+ if handler = find(event, message)
90
+ regexp, block = *handler
91
+ self.match = message.match(regexp).captures
92
+ invoke block
93
+ end
94
+ end
95
+
96
+ private
97
+ def find(type, message)
98
+ if events = @events[type]
99
+ events.detect {|regexp,_| message.match(regexp)}
100
+ end
101
+ end
102
+
103
+ def invoke(block)
104
+ mc = class << self; self; end
105
+ mc.send :define_method, :__isaac_event_handler, &block
106
+
107
+ # -1 splat arg, send everything
108
+ # 0 no args, send nothing
109
+ # 1 defined number of args, send only those
110
+ bargs = case block.arity <=> 0
111
+ when -1; match
112
+ when 0; []
113
+ when 1; match[0..block.arity-1]
114
+ end
115
+
116
+ catch(:halt) { __isaac_event_handler(*bargs) }
117
+ end
118
+ end
119
+
120
+ class IRC
121
+ def initialize(bot, config)
122
+ @bot, @config = bot, config
123
+ @transfered = 0
124
+ @registration = []
125
+ end
126
+
127
+ def connect
128
+ tcp_socket = TCPSocket.open(@config.server, @config.port)
129
+
130
+ if @config.ssl
131
+ begin
132
+ require 'openssl'
133
+ rescue ::LoadError
134
+ raise(RuntimeError,"unable to require 'openssl'",caller)
135
+ end
136
+
137
+ ssl_context = OpenSSL::SSL::SSLContext.new
138
+ ssl_context.verify_mode = OpenSSL::SSL::VERIFY_NONE
139
+
140
+ unless @config.environment == :test
141
+ puts "Using SSL with #{@config.server}:#{@config.port}"
142
+ end
143
+
144
+ @socket = OpenSSL::SSL::SSLSocket.new(tcp_socket, ssl_context)
145
+ @socket.sync = true
146
+ @socket.connect
147
+ else
148
+ @socket = tcp_socket
149
+ end
150
+
151
+ @queue = Queue.new(@socket, @bot.config.server)
152
+ message "PASS #{@config.password}" if @config.password
153
+ message "NICK #{@config.nick}"
154
+ message "USER #{@config.nick} 0 * :#{@config.realname}"
155
+ @queue.lock
156
+
157
+ while line = @socket.gets
158
+ parse line
159
+ end
160
+ end
161
+
162
+ def parse(input)
163
+ puts "<< #{input}" if @bot.config.verbose
164
+ msg = Message.new(input)
165
+
166
+ if ("001".."004").include? msg.command
167
+ @registration << msg.command
168
+ if registered?
169
+ @queue.unlock
170
+ @bot.dispatch(:connect)
171
+ end
172
+ elsif msg.command == "PRIVMSG"
173
+ if msg.params.last == "\001VERSION\001"
174
+ message "NOTICE #{msg.nick} :\001VERSION #{@bot.config.version}\001"
175
+ end
176
+
177
+ type = msg.channel? ? :channel : :private
178
+ @bot.dispatch(type, msg)
179
+ elsif msg.error?
180
+ @bot.dispatch(:error, msg)
181
+ elsif msg.command == "PING"
182
+ @queue.unlock
183
+ message "PONG :#{msg.params.first}"
184
+ elsif msg.command == "PONG"
185
+ @queue.unlock
186
+ else
187
+ event = msg.command.downcase.to_sym
188
+ @bot.dispatch(event, msg)
189
+ end
190
+ end
191
+
192
+ def registered?
193
+ (("001".."004").to_a - @registration).empty?
194
+ end
195
+
196
+ def message(msg)
197
+ @queue << msg
198
+ end
199
+ end
200
+
201
+ class Message
202
+ attr_accessor :raw, :prefix, :command, :params
203
+
204
+ def initialize(msg=nil)
205
+ @raw = msg
206
+ parse if msg
207
+ end
208
+
209
+ def numeric_reply?
210
+ @numeric_reply ||= !!@command.match(/^\d\d\d$/)
211
+ end
212
+
213
+ def parse
214
+ match = @raw.match(/(^:(\S+) )?(\S+)(.*)/)
215
+ _, @prefix, @command, raw_params = match.captures
216
+
217
+ raw_params.strip!
218
+ if match = raw_params.match(/:(.*)/)
219
+ @params = match.pre_match.split(" ")
220
+ @params << match[1]
221
+ else
222
+ @params = raw_params.split(" ")
223
+ end
224
+ end
225
+
226
+ def nick
227
+ return unless @prefix
228
+ @nick ||= @prefix[/^(\S+)!/, 1]
229
+ end
230
+
231
+ def user
232
+ return unless @prefix
233
+ @user ||= @prefix[/^\S+!(\S+)@/, 1]
234
+ end
235
+
236
+ def host
237
+ return unless @prefix
238
+ @host ||= @prefix[/@(\S+)$/, 1]
239
+ end
240
+
241
+ def server
242
+ return unless @prefix
243
+ return if @prefix.match(/[@!]/)
244
+ @server ||= @prefix[/^(\S+)/, 1]
245
+ end
246
+
247
+ def error?
248
+ !!error
249
+ end
250
+
251
+ def error
252
+ return @error if @error
253
+ @error = command.to_i if numeric_reply? && command[/[45]\d\d/]
254
+ end
255
+
256
+ def channel?
257
+ !!channel
258
+ end
259
+
260
+ def channel
261
+ return @channel if @channel
262
+ if regular_command? and params.first.start_with?("#")
263
+ @channel = params.first
264
+ end
265
+ end
266
+
267
+ def message
268
+ return @message if @message
269
+ if error?
270
+ @message = error.to_s
271
+ elsif regular_command?
272
+ @message = params.last
273
+ end
274
+ end
275
+
276
+ private
277
+ # This is a late night hack. Fix.
278
+ def regular_command?
279
+ %w(PRIVMSG JOIN PART QUIT).include? command
280
+ end
281
+ end
282
+
283
+ class Queue
284
+ def initialize(socket, server)
285
+ # We need server for pinging us out of an excess flood
286
+ @socket, @server = socket, server
287
+ @queue, @lock, @transfered = [], false, 0
288
+ end
289
+
290
+ def lock
291
+ @lock = true
292
+ end
293
+
294
+ def unlock
295
+ @lock, @transfered = false, 0
296
+ invoke
297
+ end
298
+
299
+ def <<(message)
300
+ @queue << message
301
+ invoke
302
+ end
303
+
304
+ private
305
+ def message_to_send?
306
+ !@lock && !@queue.empty?
307
+ end
308
+
309
+ def transfered_after_next_send
310
+ @transfered + @queue.first.size + 2 # the 2 is for \r\n
311
+ end
312
+
313
+ def exceed_limit?
314
+ transfered_after_next_send > 1472
315
+ end
316
+
317
+ def lock_and_ping
318
+ lock
319
+ @socket.print "PING :#{@server}\r\n"
320
+ end
321
+
322
+ def next_message
323
+ @queue.shift.to_s.chomp + "\r\n"
324
+ end
325
+
326
+ def invoke
327
+ while message_to_send?
328
+ if exceed_limit?
329
+ lock_and_ping; break
330
+ else
331
+ @transfered = transfered_after_next_send
332
+ @socket.print next_message
333
+ # puts ">> #{msg}" if @bot.config.verbose
334
+ end
335
+ end
336
+ end
337
+ end
338
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: isaac
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.5
4
+ version: 0.2.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Harry Vangberg
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-04-25 00:00:00 +02:00
12
+ date: 2009-12-21 00:00:00 +01:00
13
13
  default_executable:
14
14
  dependencies: []
15
15
 
@@ -20,13 +20,14 @@ executables: []
20
20
  extensions: []
21
21
 
22
22
  extra_rdoc_files:
23
- - LICENSE
23
+ - CHANGES
24
24
  - README.rdoc
25
25
  files:
26
26
  - README.rdoc
27
- - LICENSE
27
+ - CHANGES
28
28
  - isaac.gemspec
29
29
  - lib/isaac.rb
30
+ - lib/isaac/bot.rb
30
31
  has_rdoc: true
31
32
  homepage: http://github.com/ichverstehe/isaac
32
33
  licenses: []
data/LICENSE DELETED
@@ -1,6 +0,0 @@
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
- ------------------------------------------------------------------------------