isaac 0.2.5 → 0.2.6

Sign up to get free protection for your applications and to get access to all the features.
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
- ------------------------------------------------------------------------------