agile-isaac 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (6) hide show
  1. data/LICENSE +6 -0
  2. data/README.rdoc +94 -0
  3. data/isaac.gemspec +19 -0
  4. data/lib/isaac/config.rb +101 -0
  5. data/lib/isaac.rb +307 -0
  6. metadata +60 -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,19 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = "isaac"
3
+ s.version = "0.0.4"
4
+ s.date = "2008-12-17"
5
+ s.summary = "The smallish DSL for writing IRC bots"
6
+ s.email = "mike@cryingwhilecoding.com"
7
+ s.homepage = "http://github.com/agile/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", "Mike Vincent"]
12
+ s.files = ["README.rdoc",
13
+ "LICENSE",
14
+ "isaac.gemspec",
15
+ "lib/isaac/config.rb",
16
+ "lib/isaac.rb"]
17
+ s.rdoc_options = ["--main", "README.rdoc"]
18
+ s.extra_rdoc_files = ["LICENSE", "README.rdoc"]
19
+ end
@@ -0,0 +1,101 @@
1
+ module Isaac
2
+ module Config
3
+ class << self
4
+ def data
5
+ @data
6
+ end
7
+
8
+ def data=(data)
9
+ @data = data
10
+ self.dirty = false
11
+ end
12
+ alias :config= :data=
13
+
14
+ def config(network=nil)
15
+ network ||= default_network
16
+ @data.config[network] || {}
17
+ end
18
+
19
+ def networks
20
+ @data.networks
21
+ end
22
+
23
+ def default_network
24
+ networks.include?('default') ? 'default' : networks.first
25
+ end
26
+
27
+ def loaded?
28
+ !data.nil?
29
+ end
30
+
31
+ def dirty?
32
+ @dirty == true
33
+ end
34
+
35
+ def dirty=(bool)
36
+ @dirty = bool
37
+ end
38
+
39
+ def write_config(filename=nil)
40
+ File.open(filename || data.config_filename, 'w') do |file|
41
+ file << data.to_yaml
42
+ end
43
+ end
44
+
45
+ def method_missing(method, *args)
46
+ if method.to_s =~ /=$/
47
+ base_name = method.to_s.gsub(/=$/,'')
48
+ self.dirty = true
49
+ data.config[default_network][base_name] = args.first
50
+ elsif config.has_key?(method.to_s)
51
+ config[method.to_s]
52
+ end
53
+ end
54
+ end
55
+ class Data
56
+ attr_accessor :config, :config_filename
57
+
58
+ def initialize(config=nil)
59
+ self.config = read_config(config)
60
+ self.config_filename = config || default_config_file
61
+ end
62
+
63
+ def config_dir
64
+ File.exist?('config') && File.directory?('config') ? './config' : '.'
65
+ end
66
+
67
+ def default_config_file
68
+ File.join(config_dir, "config.yml")
69
+ end
70
+
71
+ def networks
72
+ config.keys
73
+ end
74
+
75
+ def read_config(config=nil)
76
+ begin
77
+ require 'erb'
78
+ require 'yaml'
79
+ rescue LoadError
80
+ retry if require 'rubygems'
81
+ end
82
+ begin
83
+ config ||= config_filename
84
+ self.config_filename = config unless config == config_filename
85
+ YAML::load(ERB.new(IO.read(config)).result)
86
+ rescue
87
+ {"default" => {}}
88
+ end
89
+ end
90
+
91
+ def to_yaml
92
+ config.to_yaml
93
+ end
94
+
95
+ def self.load(config=nil)
96
+ Isaac::Config.data = new(config)
97
+ end
98
+ end
99
+ end
100
+ end
101
+
data/lib/isaac.rb ADDED
@@ -0,0 +1,307 @@
1
+ require 'socket'
2
+ require 'isaac/config'
3
+ require 'logger'
4
+
5
+ module Isaac
6
+
7
+ # Returns the current instance of Isaac::Application
8
+ def self.app
9
+ @app ||= Application.new
10
+ end
11
+
12
+ # Use EventContext methods such as msg(), join() etc. outside on()-events. See +examples/execute.rb+.
13
+ # Isaac.execute do
14
+ # msg 'harryjr', 'you're awesome'
15
+ # end
16
+ def self.execute(params={}, &block)
17
+ app.execute(params, &block)
18
+ end
19
+
20
+ def self.logger
21
+ @logger ||= Logger.new(Config.log_file || "isaac.log")
22
+ end
23
+
24
+ #Config = Struct.new(:nick, :server, :port, :server_pass, :username, :realname, :version, :verbose, :nick_pass)
25
+
26
+ # These are top level methods you use to construct your bot.
27
+ class Application
28
+ def initialize #:nodoc:
29
+ @events = Hash.new {|k,v| k[v] = []}
30
+ @registration = []
31
+ end
32
+
33
+ def logger
34
+ Isaac.logger
35
+ end
36
+
37
+ # This is plain stupid. Might be useful for logging or something later on.
38
+ def start #:nodoc:
39
+ logger.info " ==== Starting Isaac ==== "
40
+ connect
41
+ logger.info " ==== Ending Isaac ==== "
42
+ end
43
+
44
+ # Configure the bot:
45
+ # defaults to the 'default' or first network
46
+ #
47
+ # config do |c|
48
+ # c.server = "irc.freenode.net"
49
+ # c.nick = "AwesomeBot"
50
+ # c.port = 6667
51
+ # c.realname = "James Dean"
52
+ # c.username = "jdean"
53
+ # c.version = "James Dean Bot v2.34"
54
+ # c.verbose = true
55
+ # c.server_pass = "secrets_for_server_connections"
56
+ # c.nick_pass = "secrets_for_nickserv"
57
+ # end
58
+ #
59
+ # config("mynet") do |c|
60
+ # c.server = "irc.example.com"
61
+ # c.port = 6667
62
+ # c.nick = "larry"
63
+ # end
64
+ #
65
+ # Can also be used as an accessor to configuration:
66
+ # config.nick => "AwesomeBot"
67
+ # config("mynet").nick => "larry"
68
+ #
69
+ def config(&block)
70
+ Config::Data.load unless Config.loaded?
71
+ block.call(Config) if block_given?
72
+ Config
73
+ end
74
+
75
+ # Methods defined inside the helpers-block will be available to on()-events at execution time.
76
+ def helpers(&block)
77
+ EventContext.class_eval(&block)
78
+ end
79
+
80
+ # on()-events responds to certain actions. Depending on +type+ certain local variables are available:
81
+ # +nick+, +channel+, +message+ and in particular +match+, which contains a MatchData object returned
82
+ # by the given regular expression.
83
+ #
84
+ # * Do something after connection has been established, e.g. join channels.
85
+ # on :connect do
86
+ # join "#awesome_channel", "#lee_marvin_fans"
87
+ # end
88
+ # * Respond to private messages matching a given regular expression.
89
+ # on :private, /^echo (.*)/ do
90
+ # msg nick, "You said '#{match[1]}!"
91
+ # end
92
+ # * Respond to messages matching a given regular expression send to a channel.
93
+ # on :channel, /quote/ do
94
+ # msg channel, "#{nick} requested a quote: 'Smoking, a subtle form a suicide.' - Vonnegut"
95
+ # end
96
+ # * Respond to error codes, according to the RFC.
97
+ # on :error, 401 do
98
+ # # Execute this if you try to send a message to a non-existing nick/channel.
99
+ # end
100
+ def on(type, match=nil, &block)
101
+ @events[type] << e = Event.new(match, block)
102
+ return e
103
+ end
104
+
105
+ def execute(params={}, &block) #:nodoc:
106
+ event = Event.new(:dsl, block)
107
+ @queue << event.invoke(params)
108
+ end
109
+
110
+ def event(type, matcher)
111
+ @events[type].detect do |e|
112
+ type == :error ? matcher == e.match : matcher =~ e.match
113
+ end
114
+ end
115
+
116
+ def connect
117
+ begin
118
+ logger.info "Connecting to #{config.server} at port #{config.port}"
119
+ @irc = TCPSocket.open(config.server, config.port)
120
+ logger.info "Connection established."
121
+
122
+ @irc.puts "PASS #{config.server_pass}" if config.server_pass
123
+ @irc.puts "NICK #{config.nick}"
124
+ @irc.puts "USER #{config.username} foobar foobar :#{config.realname}"
125
+
126
+ @queue = Queue.new(@irc)
127
+ @queue << @events[:connect].first.invoke if @events[:connect].first
128
+
129
+ while line = @irc.gets
130
+ handle line
131
+ end
132
+ rescue Interrupt => e
133
+ puts "Disconnected! An error occurred: #{e.inspect}"
134
+ #rescue Timeout::Error => e
135
+ # puts "Timeout: #{e}. Reconnecting."
136
+ # connect
137
+ end
138
+ end
139
+
140
+ def registered?
141
+ arr = [1,2,3,4] - @registration
142
+ arr.empty?
143
+ end
144
+
145
+ # This is one hell of a nasty method. Something should be done, I suppose.
146
+ def handle(line)
147
+ puts "> #{line}" if config.verbose
148
+
149
+ case line
150
+ when /^:(\S+)!\S+ PRIVMSG \S+ :?\001VERSION\001/
151
+ @queue << "NOTICE #{$1} :\001VERSION #{config.version}\001"
152
+ when /^:(\S+)!(\S+) PRIVMSG (\S+) :?(.*)/
153
+ nick, userhost, channel, message = $1, $2, $3, $4
154
+ type = channel.match(/^#/) ? :channel : :private
155
+ if event = event(type, message)
156
+ @queue << event.invoke(:nick => nick, :userhost => userhost, :channel => channel, :message => message)
157
+ end
158
+ when /^:\S+ 00([1-4])/
159
+ @registration << $1.to_i
160
+ @queue.lock = false if registered?
161
+ when /^:\S+ ([4-5]\d\d) \S+ (\S+)/
162
+ error = $1
163
+ nick = channel = $2
164
+ if event = event(:error, error.to_i)
165
+ @queue << event.invoke(:nick => nick, :channel => channel)
166
+ end
167
+ when /^PING (\S+)/
168
+ #TODO not sure this is correct. Damned RFC.
169
+ @queue << "PONG #{$1}"
170
+ when /^:\S+ PONG \S+ :excess/
171
+ @queue.lock = false
172
+ end
173
+ end
174
+ end
175
+
176
+ class Queue #:nodoc:
177
+ attr_accessor :lock
178
+ def initialize(socket)
179
+ @socket = socket
180
+ @queue = []
181
+ @transfered = 0
182
+ @lock = true
183
+ transmit
184
+ end
185
+
186
+ # I luvz Rubyz
187
+ def << (msg)
188
+ # .flatten! returns nill if no modifications were made, thus we do this.
189
+ @queue = (@queue << msg).flatten
190
+ end
191
+
192
+ # To prevent excess flood no more than 1472 bytes will be sent to the
193
+ # server. When that limit is reached, @lock = true and the server will be
194
+ # PINGed. @lock will be true until a PONG is received (Application#handle).
195
+ def transmit
196
+ Thread.start { loop {
197
+ unless @lock || @queue.empty?
198
+ msg = @queue.first
199
+ if (@transfered + msg.size) > 1472
200
+ # No honestly, :excess. The RFC is not too clear on this subject TODO
201
+ @socket.puts "PING :excess"
202
+ @lock = true
203
+ @transfered = 0
204
+ else
205
+ @socket.puts msg
206
+ @transfered += msg.size
207
+ @queue.shift
208
+ end
209
+ end
210
+ sleep 0.1
211
+ }}
212
+ end
213
+ end
214
+
215
+ class Event #:nodoc:
216
+ attr_accessor :match, :block
217
+ def initialize(match, block)
218
+ @match = match
219
+ @block = block
220
+ end
221
+
222
+ # Execute event in the context of EventContext.
223
+ def invoke(params={})
224
+ match = params[:message].match(@match) if @match && params[:message]
225
+ params.merge!(:match => match)
226
+
227
+ context = EventContext.new(params)
228
+ context.instance_eval(&@block)
229
+ context.commands
230
+ end
231
+ end
232
+
233
+ class EventContext
234
+ attr_accessor :nick, :userhost, :channel, :message, :match, :commands
235
+ def initialize(args = {})
236
+ args.each {|k,v| instance_variable_set("@#{k}",v)}
237
+ @commands = []
238
+ end
239
+
240
+ # Send a raw IRC message.
241
+ def raw(command)
242
+ @commands << command
243
+ end
244
+
245
+ # Send a message to nick/channel.
246
+ def msg(recipient, text)
247
+ raw("PRIVMSG #{recipient} :#{text}")
248
+ end
249
+
250
+ # Send a notice to nick/channel
251
+ def notice(recipient, text)
252
+ raw("PRIVMSG #{recipient} :#{text}")
253
+ end
254
+
255
+ # Join channel(s):
256
+ # join "#awesome_channel"
257
+ # join "#rollercoaster", "#j-lo"
258
+ def join(*channels)
259
+ channels.each {|channel| raw("JOIN #{channel}")}
260
+ end
261
+
262
+ # Part channel(s):
263
+ # part "#awesome_channel"
264
+ # part "#rollercoaster", "#j-lo"
265
+ def part(*channels)
266
+ channels.each {|channel| raw("PART #{channel}")}
267
+ end
268
+
269
+ # Kick nick from channel, with optional comment.
270
+ def kick(channel, nick, comment=nil)
271
+ comment = " :#{comment}" if comment
272
+ raw("KICK #{channel} #{nick}#{comment}")
273
+ end
274
+
275
+ # Change topic of channel.
276
+ def topic(channel, topic)
277
+ raw("TOPIC #{channel} :#{topic}")
278
+ end
279
+
280
+ # Invite nicks to channel
281
+ # invite "#awesome_channel", "arnie"
282
+ # invite "#awesome_channel", "arnie", "brigitte"
283
+ def invite(channel, *nicks)
284
+ nicks.each {|nick| raw("INVITE #{nick} #{channel}")}
285
+ end
286
+
287
+ # Change nickname
288
+ def newnick(nickname)
289
+ raw("NICK #{nickname}")
290
+ end
291
+ end
292
+ end
293
+
294
+ # Assign methods to current Isaac instance
295
+ %w(config helpers on).each do |method|
296
+ eval(<<-EOF)
297
+ def #{method}(*args, &block)
298
+ Isaac.app.#{method}(*args, &block)
299
+ end
300
+ EOF
301
+ end
302
+
303
+ # Clever, thanks Sinatra.
304
+ at_exit do
305
+ raise $! if $!
306
+ Isaac.app.start
307
+ end
metadata ADDED
@@ -0,0 +1,60 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: agile-isaac
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.4
5
+ platform: ruby
6
+ authors:
7
+ - Harry Vangberg
8
+ - Mike Vincent
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2008-12-17 00:00:00 -08:00
14
+ default_executable:
15
+ dependencies: []
16
+
17
+ description: Small DSL for writing IRC bots.
18
+ email: mike@cryingwhilecoding.com
19
+ executables: []
20
+
21
+ extensions: []
22
+
23
+ extra_rdoc_files:
24
+ - LICENSE
25
+ - README.rdoc
26
+ files:
27
+ - README.rdoc
28
+ - LICENSE
29
+ - isaac.gemspec
30
+ - lib/isaac/config.rb
31
+ - lib/isaac.rb
32
+ has_rdoc: true
33
+ homepage: http://github.com/agile/isaac
34
+ post_install_message:
35
+ rdoc_options:
36
+ - --main
37
+ - README.rdoc
38
+ require_paths:
39
+ - lib
40
+ required_ruby_version: !ruby/object:Gem::Requirement
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ version: "0"
45
+ version:
46
+ required_rubygems_version: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: "0"
51
+ version:
52
+ requirements: []
53
+
54
+ rubyforge_project: isaac
55
+ rubygems_version: 1.2.0
56
+ signing_key:
57
+ specification_version: 2
58
+ summary: The smallish DSL for writing IRC bots
59
+ test_files: []
60
+