cereal 0.1.8

Sign up to get free protection for your applications and to get access to all the features.
Files changed (9) hide show
  1. data/CHANGES +57 -0
  2. data/MIT-LICENSE +21 -0
  3. data/README +86 -0
  4. data/Rakefile +30 -0
  5. data/TODO +9 -0
  6. data/lib/cereal.rb +423 -0
  7. data/lib/connection.rb +404 -0
  8. data/lib/user.rb +98 -0
  9. metadata +71 -0
data/CHANGES ADDED
@@ -0,0 +1,57 @@
1
+ 2009-03-30 v0.1.8 ross
2
+
3
+ * lib/connection.rb (handle_line): Curbed a nasty memory leak by manually instantiating garbage collection. Residential memory usage should be more stable now.
4
+
5
+ 2009-03-13 v0.1.7 ross
6
+
7
+ * lib/cereal.rb: All colors and text attributes now work correctly! Yay!
8
+
9
+ 2009-03-13 v0.1.6 ross
10
+
11
+ * lib/cereal.rb (confirm_nick): Added @connection.temp_nick assignment.
12
+
13
+ * lib/connection.rb: Added attr_accessor for @temp_nick.
14
+
15
+ Fixes #6.
16
+
17
+ 2009-02-23 v0.1.5 ross
18
+
19
+ * lib/cereal.rb (kick, ban): Methods added.
20
+
21
+ 2009-02-23 v0.1.4 ross
22
+
23
+ * lib/connection.rb (initialize, receive_data): Added @incomplete_line_buffer to hold incomplete lines received from the server (lines not ending in a CR-LF or \r\n pair) until we receive the complete line. This should fix issues with large channels not reporting the correct number of users, etc.
24
+
25
+ 2009-02-23 v0.1.3 ross
26
+
27
+ * lib/connection.rb (send_data): Added simplest fix ever that finally rids us of EventMachine complaining about sending data after closing a connection. OH GOD, THIS IS SO SATISFYING.
28
+
29
+ * lib/cereal.rb: Updated version number to 0.1.3.
30
+
31
+ 2009-02-21 v0.1.2 jon
32
+
33
+ * lib/connection.rb (handle_line): Moved on_quit callback to begining of block. This allows any on_quit callback to find the channels that the user was in before being removed from the channel list. (Might want a better way to do this, possibly grab a list of channels the user was in and pass it as an argument? Same thing with other blocks such as nick change.)
34
+
35
+ 2009-02-21 v0.1.2 ross
36
+
37
+ * cereal.gemspec: Modified to pull gem version straight from lib/cereal.rb. No more updating! Also added auto updating of spec.date.
38
+
39
+ 2009-02-21 v0.1.2 ross
40
+
41
+ * lib/cereal.rb (channels): Added. Method to grab hash of currently inhabited channels.
42
+
43
+ 2009-02-21 v0.1.1 ross
44
+
45
+ * cereal.gemspec: Changed gem version to 0.1.1 to match actual Cereal version!
46
+
47
+ 2009-02-21 v0.1.1 ross
48
+
49
+ * lib/cereal.rb (get_users): Added. Method to grab array of Cereal::User objects for a given channel that the bot is in.
50
+
51
+ * lib/connection.rb: Added attr_reader for @channels. Used by Cereal::Bot#get_users.
52
+
53
+ * cereal.gemspec: Added Jon R. to authors list.
54
+
55
+ 2009-02-17 ross
56
+
57
+ * Initial import to SVN!
@@ -0,0 +1,21 @@
1
+ The MIT License
2
+
3
+ Copyright (c) 2009 Ross Paffett
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,86 @@
1
+ = Cereal
2
+ This package contains Cereal, an event-driven IRC framework built on
3
+ EventMachine and inspired by Paul Mutton's excellent
4
+ PircBot[http://jibble.org/pircbot.php] IRC framework for Java.
5
+
6
+ Cereal has the following features:
7
+
8
+ * Easy to use! Cereal is designed for quick-and-easy implementation, just like
9
+ PircBot. Your first bot can be set up by overriding a single class.
10
+
11
+ * Your specific functionality is easily added by overriding logically-named
12
+ methods, like +on_message+.
13
+
14
+ Be sure not to confuse _Cereal_ and _CerealBot_. Cereal is a standalone IRC
15
+ framework. By itself, Cereal does nothing. CerealBot[http://cerealbot.org],
16
+ on the other hand, is a fully-featured IRC bot project by the same people who
17
+ have ruined your life with Cereal.
18
+
19
+ == Authors
20
+ [Ross "Raws" Paffett] Lead developer.
21
+ [Brian "TecnoBrat" S.] Inspiration, Ruby knowledge, general abuse.
22
+ [Jon "Corgan" R.] Butts.
23
+
24
+ == Download
25
+ Cereal is currently hosted at cerealbot.org[http://cerealbot.org/].
26
+
27
+ == Installation
28
+
29
+ === Gem Installation from Source
30
+ Generate a Cereal .gem file and install it with the following commands:
31
+
32
+ rake package
33
+ gem install --local pkg/cereal
34
+
35
+ Cereal will now be available to your Ruby code through rubygems:
36
+
37
+ require 'rubygems'
38
+ require 'cereal'
39
+
40
+ You can clean up the package rake task files with:
41
+
42
+ rake clobber_package
43
+
44
+ ==== Generate Documentation
45
+ You may generate documentation from source with the following command:
46
+
47
+ rake rdoc
48
+
49
+ RDoc documentation will be created in the directory "doc".
50
+
51
+ You can clean up the rdoc rake task files with:
52
+
53
+ rake clobber_rdoc
54
+
55
+ == Simple Example
56
+ Once installed, you can create your first bot!
57
+
58
+ require 'rubygems'
59
+ require 'cereal'
60
+
61
+ class MyBot < Cereal::Bot
62
+ def initialize
63
+ super
64
+ @name = 'My Bot'
65
+ @nick = 'MyBot'
66
+ @verbose = true
67
+ end
68
+
69
+ def on_connect(host)
70
+ puts "Connected to #{host}!"
71
+
72
+ join('#cerealbot')
73
+
74
+ msg('#cerealbot', 'Hi!')
75
+ msg('Corgan', 'Hello!')
76
+ end
77
+ end
78
+
79
+ bot = MyBot.new
80
+ bot.connect('irc.freenode.net')
81
+
82
+ == Documentation
83
+ See the RDoc documentation for the Cereal module for more information.
84
+
85
+ == License
86
+ Cereal is available under the MIT license. See the file MIT-LICENSE.
@@ -0,0 +1,30 @@
1
+ # Rakefile for Cereal
2
+ #
3
+ # Copyright (c) 2009 Ross Paffett. Licensed under the MIT license:
4
+ # http://www.opensource.org/licenses/mit-license.php
5
+
6
+ require 'rubygems'
7
+ require 'rake/packagetask'
8
+ require 'rake/gempackagetask'
9
+ require 'rake/rdoctask'
10
+
11
+ load 'cereal.gemspec'
12
+
13
+ # Generate rdoc documentation in ./doc/.
14
+ Rake::RDocTask.new do |rd|
15
+ rd.rdoc_dir = 'doc'
16
+ rd.main = 'README'
17
+ rd.rdoc_files.include('MIT-LICENSE', 'README', 'TODO', 'lib/**/*.rb')
18
+ end
19
+
20
+ # Create a Ruby Gem package.
21
+ Rake::GemPackageTask.new(@@spec) do |pkg|
22
+ pkg.need_zip = false
23
+ pkg.need_tar = false
24
+ end
25
+
26
+ # Package Cereal source into tarball
27
+ Rake::PackageTask.new(@@spec.name, @@spec.version) do |p|
28
+ p.need_tar_gz = true
29
+ p.package_files.include('**/**')
30
+ end
data/TODO ADDED
@@ -0,0 +1,9 @@
1
+ = Cereal -- To Do List
2
+
3
+ Send suggestions for this list to mailto:nobody@inparticul.ar! Actually, there
4
+ is someone, but he doesn't have a proper public address set up yet.
5
+
6
+ === To Do
7
+ * Document the rest of the library.
8
+ * Write tests (is this even practically possible?).
9
+ * Figure out if there's anything else to do.
@@ -0,0 +1,423 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # == Synopsis
4
+ # Cereal is an IRC connection framework inspired (quite heavily) by
5
+ # Paul Mutton's excellent PircBot[http://jibble.org/pircbot.php]
6
+ # IRC library for Java.
7
+ #
8
+ # See documentation for the Cereal module for more information.
9
+ #
10
+ # == Author
11
+ # Ross "Raws" Paffett
12
+ #
13
+ # == Copyright
14
+ # Copyright (c) 2009 Ross Paffett. Licensed under the MIT license:
15
+ # http://www.opensource.org/licenses/mit-license.php
16
+
17
+ # Standard library
18
+ require 'ostruct'
19
+ require 'thread'
20
+
21
+ # Rubygems
22
+ require 'rubygems'
23
+ require 'eventmachine'
24
+
25
+ # Cereal library
26
+ require 'connection.rb'
27
+ require 'user.rb'
28
+
29
+ # == Overview
30
+ # Cereal is a Ruby framework for quickly and easily constructing IRC applications.
31
+ #
32
+ # TODO Actually write documentation or something. See Cereal::Bot for some
33
+ # up-and-coming documentation for now.
34
+ module Cereal
35
+ # Definitive version number of this release of Cereal.
36
+ VERSION = "0.1.8"
37
+
38
+ # Colors
39
+ NORMAL = N = "\017"
40
+ BOLD = B = "\002"
41
+ UNDERLINE = UL = "\037"
42
+ REVERSE = REV = ITALIC = IT = "\026"
43
+ WHITE = "\0030"
44
+ BLACK = "\0031"
45
+ DARK_BLUE = NAVY = "\0032"
46
+ DARK_GREEN = FOREST = "\0033"
47
+ RED = "\0034"
48
+ MAROON = BROWN = "\0035"
49
+ PURPLE = "\0036"
50
+ ORANGE = OLIVE = "\0037"
51
+ YELLOW = "\0038"
52
+ GREEN = "\0039"
53
+ TEAL = "\00310"
54
+ CYAN = "\00311"
55
+ BLUE = "\00312"
56
+ MAGENTA = "\00313"
57
+ DARK_GRAY = "\00314"
58
+ LIGHT_GRAY = ASH = "\00315"
59
+
60
+ class Bot
61
+
62
+ attr_accessor :name, :login, :version, :finger, :message_delay, :verbose
63
+ attr_reader :nick
64
+
65
+ def initialize
66
+ @name = 'Cereal'
67
+ @nick = @name
68
+ @login = 'cereal'
69
+ @version = "Cereal #{Cereal::VERSION} (Ruby #{RUBY_VERSION})"
70
+ @finger = 'You ought to be arrested for fingering a bot!'
71
+ @message_delay = 1.0
72
+ @verbose = false
73
+ end
74
+
75
+ # Connect to a server, optionally specifying a port and password. This method
76
+ # starts the "main event loop." You may want to rescue +IrcError+ and/or
77
+ # +NickAlreadyInUseError+ here, as they may be raised upon a connection failure
78
+ # or other trouble.
79
+ # [_server_] The server hostname to connect to.
80
+ # [[_port_]] The port to connect to. Most IRC servers use port 6667 or similar.
81
+ # [[_password_]] The password to connect to the server with. A value of +nil+
82
+ # (the default) will cause Cereal to skip this step.
83
+ def connect(server, port=6667, password=nil)
84
+ EventMachine::run {
85
+ @connection = EventMachine::connect(server, port, Cereal::Connection,
86
+ :bot => self, :password => password)
87
+ }
88
+ end
89
+
90
+ # Add a line to the log. The default behavior is to output to STDOUT using the
91
+ # "PircBot" log format, which is recognized by pisg[http://pisg.sourceforge.net/],
92
+ # the Perl IRC Statistics Generator. This is only done if +@verbose+ is set to +true+.
93
+ #
94
+ # The PircBot log format is described as each line beginning with an integer that
95
+ # is the number of milliseconds since the epoch, followed by a single space, " ",
96
+ # then the log message. Outgoing messages are prefixed by ">>>" immediately
97
+ # following the space character after the timestamp:
98
+ #
99
+ # 1233871496459 >>>NICK Wheaties
100
+ # 1233871496459 >>>USER wheaties 0 * :Botfast of Champions!
101
+ # 1233871496488 :ravaged.mule.anus 001 Wheaties :Welcome to the Internet Relay Network Wheaties!~wheaties@Stig
102
+ # [_line_] The line to add to the log.
103
+ def log(line)
104
+ puts (Time.now.to_f * 1000).to_i.to_s + " #{line}" if @verbose
105
+ end
106
+
107
+ # Returns an array of Cereal::User objects representing the users in the specified
108
+ # channel. Note that the bot must be in a channel to be aware of its users. In addition,
109
+ # this method may return an incomplete list of the bot has not received the entire user
110
+ # list for a channel immediately after joining. To be sure you receive a full list, you
111
+ # may want to override on_user_list instead.
112
+ # [_channel_] The channel to fetch an array of Cereal::User objects for.
113
+ def get_users(channel)
114
+ channel.downcase!
115
+ @connection.channels.fetch(channel, {}).keys
116
+ end
117
+
118
+ # Returns a hash of channels that the bot is currently in. The hash's keys are strings representing
119
+ # the channel name, so that <code>channels.keys</code> will return an array of channel name strings.
120
+ # The hash's values are arrays of Cereal::User objects representing the users in that channel, which
121
+ # is the same data returned by get_users.
122
+ def channels
123
+ @connection.channels
124
+ end
125
+
126
+ # Join a channel with an optional key.
127
+ # [_channel_] The channel to join.
128
+ # [[_key_]] The key that will be used to join the channel.
129
+ def join(channel, key=nil)
130
+ send_raw("JOIN #{channel}" + (key.nil? ? '' : " #{key}"))
131
+ end
132
+
133
+ # Part a channel with an optional reason.
134
+ # [_channel_] The channel to leave.
135
+ # [[_reason_]] The reason for parting the channel.
136
+ def part(channel, reason=nil)
137
+ send_raw("PART #{channel}" + (reason.nil? ? '' : " :#{reason}"))
138
+ end
139
+
140
+ # Quit from the IRC server with an optional reason.
141
+ # [[_reason_]] The reason for quitting the server.
142
+ def quit(reason = '')
143
+ @connection.send_data("QUIT :#{reason}")
144
+ end
145
+
146
+ # Attempt to kick a user from a channel, optionally giving a reason. This
147
+ # action may require the bot have operator status in the channel.
148
+ # [_channel_] The channel to kick the user from.
149
+ # [_nick_] The nick of the user to kick.
150
+ # [[_reason_]] The reason for kicking the user.
151
+ def kick(channel, nick, reason='')
152
+ send_raw("KICK #{channel} #{nick} :#{reason}")
153
+ end
154
+
155
+ # Ban a user from a channel. An example of a valid hostmask is "*!*compu@*.18hp.net".
156
+ # This may be used in conjunction with the kick method to permanently remove a user
157
+ # from a channel. This action may require the bot have operator status in the channel.
158
+ # Some IRC networks allow banning by nick in adddition to hostmask.
159
+ # [_channel_] The channel to ban the user from.
160
+ # [_hostmask_] A hostmask representing the user we're banning.
161
+ def ban(channel, hostmask)
162
+ send_raw("MODE #{channel} +b #{hostmask}")
163
+ end
164
+
165
+ # Send a message to a channel or a private message to a user.
166
+ #
167
+ # # Send the message "Hello!" to the channel #perkele.
168
+ # send_message('#perkele', 'Hello!')
169
+ #
170
+ # # Send a private message to Jibbler that says "Hi!"
171
+ # send_message('Jibbler', 'Hi!')
172
+ # [_target_] The channel or user nick to send to.
173
+ # [_message_] The message to send.
174
+ def send_message(target, message)
175
+ send_raw("PRIVMSG #{target} :#{message}")
176
+ end
177
+
178
+ alias :message :send_message
179
+ alias :msg :send_message
180
+ alias :pm :send_message
181
+
182
+ # Send an action to a channel or user.
183
+ # [_target_] The channel or user to send to.
184
+ # [_action_] The action to send.
185
+ def send_action(target, action)
186
+ send_ctcp(target, "ACTION #{action}")
187
+ end
188
+
189
+ alias :action :send_action
190
+
191
+ # send is a convenience combination of send_message and
192
+ # send_action. If _message_ begins with "/me ", Cereal sends
193
+ # an ACTION to _target_. Otherwise, a regular message is sent.
194
+ # [_target_] The channel or user to send to.
195
+ # [_message_] The message to send. If _message_ begins with
196
+ # "/me ", an ACTION is sent.
197
+ def send(target, message)
198
+ if message =~ /^\/me (.*)$/
199
+ send_action(target, $~[1])
200
+ else
201
+ send_message(target, message)
202
+ end
203
+ end
204
+
205
+ # Send a notice to a channel or user.
206
+ # [_target_] The channel or user to send to.
207
+ # [_notice_] The notice to send.
208
+ def send_notice(target, notice)
209
+ send_raw("NOTICE #{target} :#{notice}")
210
+ end
211
+
212
+ alias :notice :send_notice
213
+
214
+ # Send a CTCP (client-to-client protocol) command to a channel
215
+ # or user. Examples of such commands are <code>PING <number></code>,
216
+ # +FINGER+, +VERSION+, etc.
217
+ #
218
+ # # Request the version of the user DeadEd
219
+ # send_ctcp('DeadEd', 'VERSION')
220
+ # [_target_] The channel or user to send the CTCP message to.
221
+ # [_command_] The CTCP command to send.
222
+ def send_ctcp(target, command)
223
+ send_raw("PRIVMSG #{target} :\1#{command}\1")
224
+ end
225
+
226
+ alias :ctcp :send_ctcp
227
+
228
+ # Change the current nick of the bot. If offline, this method will set
229
+ # the nick or nicks the bot will try to use upon a connection attempt.
230
+ # If connected, this method will attempt to change the current nick of
231
+ # the bot. #nick will return the new nick after the bot receives
232
+ # confirmation of a successful nick change from the IRC server.
233
+ #
234
+ # [_new_nick_] The new nick to use. Can be a String or an Array of
235
+ # Strings. If passed an Array, the bot will try each nick
236
+ # in order from first to last until it finds a nick that
237
+ # is not in use.
238
+ def nick=(new_nick)
239
+ if connected?
240
+ new_nick = new_nick.shift.to_s if new_nick.is_a?(Array)
241
+ send_raw("NICK #{new_nick}")
242
+ else
243
+ @nick = new_nick
244
+ end
245
+ end
246
+
247
+ alias :change_nick :nick=
248
+
249
+ # Used by Cereal::Connection to change @nick after we've received a confirmation
250
+ # that our requested nick has been assigned to us.
251
+ # [_confirmed_nick_] The nick that has been assigned to us.
252
+ def confirm_nick(confirmed_nick)
253
+ @nick = confirmed_nick
254
+ @connection.temp_nick = confirmed_nick
255
+ end
256
+
257
+ # Returns _true_ if the bot is currently connected to a server, and _false_
258
+ # if otherwise (even if the bot is currently negotiating a connection).
259
+ def connected?
260
+ @connection && @connection.state == Cereal::Connection::STATE_CONNECTED
261
+ end
262
+
263
+ # Send raw data to the IRC server.
264
+ # [_data_] The data to send to the server.
265
+ def send_raw(data)
266
+ @connection.send_raw(data) if !data.nil?
267
+ end
268
+
269
+ alias :<< :send_raw
270
+
271
+ def on_action(sender, target, action)
272
+
273
+ end
274
+
275
+ def on_channel_info(channel, user_count, topic)
276
+
277
+ end
278
+
279
+ def on_connect(host)
280
+
281
+ end
282
+
283
+ def on_disconnect
284
+
285
+ end
286
+
287
+ def on_finger(sender, target)
288
+ send_raw "NOTICE #{sender.nick} :\1FINGER #{@finger}\1"
289
+ end
290
+
291
+ def on_invite(sender, target, channel)
292
+
293
+ end
294
+
295
+ def on_join(sender, channel)
296
+
297
+ end
298
+
299
+ def on_kick(sender, channel, recipient, reason)
300
+
301
+ end
302
+
303
+ def on_message(sender, target, message)
304
+
305
+ end
306
+
307
+ def on_nick_change(sender, new_nick)
308
+
309
+ end
310
+
311
+ def on_notice(sender, target, notice)
312
+
313
+ end
314
+
315
+ def on_part(sender, channel)
316
+
317
+ end
318
+
319
+ def on_ping(sender, target, ping_value)
320
+ send_raw("NOTICE #{sender.nick} :\1PING #{ping_value}\1")
321
+ end
322
+
323
+ def on_private_message(sender, message)
324
+
325
+ end
326
+
327
+ def on_quit(sender, reason)
328
+
329
+ end
330
+
331
+ def on_server_ping(response)
332
+ @connection.send_data("PONG #{response}")
333
+ end
334
+
335
+ # Called when we receive a numeric response from the IRC server. Numeric
336
+ # replies are received in response to commands sent to the server, and are
337
+ # documented in RFC 2812, Section 5, "Replies".
338
+ #
339
+ # For example, we can use this method to discover the topic of a channel
340
+ # when we join it. If we join the channel ##test which has a topic of
341
+ # "My spoon is too big" then the _response_ will be <code>"Cereal ##test
342
+ # :My spoon is too big"</code> with a _code_ of 332 to signify that this
343
+ # is a topic. (Note that this is just an example, and that overriding
344
+ # on_topic is an easier way of finding the topic for a channel.)
345
+ # [_code_] Integer numeric code for the response.
346
+ # [_response_] Full response string from the IRC server.
347
+ def on_server_response(code, response)
348
+
349
+ end
350
+
351
+ def on_time(sender, target)
352
+ send_raw("NOTICE #{sender.nick} :\1TIME #{Time.new}\1")
353
+ end
354
+
355
+ # Called whenever a user sets the topic of a channel, or when Cereal
356
+ # joins a new channel and discovers its topic.
357
+ # [_set_by_] The nick of the user that set the topic.
358
+ # [_channel_] The channel whose topic has been set or discovered.
359
+ # [_date_] When the topic was set as a +Time+.
360
+ # [_topic_] The topic for the channel.
361
+ # [_changed_] True if the topic has just been changed, false if we're
362
+ # simply discovering a new channel's topic.
363
+ def on_topic(set_by, channel, date, topic, changed)
364
+
365
+ end
366
+
367
+ # Called whenever we receive a line from the IRC server that Cereal
368
+ # does not recognize.
369
+ # [_line_] The raw line that was received from the server.
370
+ def on_unknown(line)
371
+
372
+ end
373
+
374
+ # Called when we receive a user list from the server after joining a
375
+ # channel.
376
+ #
377
+ # After joining a channel, Cereal collects this information
378
+ # provided by the IRC server and calls this method as soon as it
379
+ # has the full list.
380
+ # [_channel_] The name of the channel.
381
+ # [_users_] An array of User objects residing in _channel_.
382
+ def on_user_list(channel, users)
383
+
384
+ end
385
+
386
+ # Called whenever we receive a VERSION CTCP request. The default
387
+ # implementation responds with Cereal's generic version string,
388
+ # so if you override this method, be sure to either mimic its
389
+ # functionality or call <code>super(...)</code>.
390
+ # [_sender_] An OpenStruct object describing the sender of this
391
+ # request. See on_message.
392
+ # [_target_] The target of the VERSION request, be it our nick
393
+ # or a channel name.
394
+ def on_version(sender, target)
395
+ send_raw("NOTICE #{sender.nick} :\1VERSION #{@version}\1")
396
+ end
397
+
398
+ end
399
+
400
+ # == Synopsis
401
+ # An IRC error class.
402
+ #
403
+ # == Author
404
+ # Ross "Raws" Paffett
405
+ #
406
+ # == Copyright
407
+ # Copyright (c) 2009 Ross Paffett. Licensed under the MIT license:
408
+ # http://www.opensource.org/licenses/mit-license.php
409
+ class IrcError < StandardError; end
410
+
411
+ # == Synopsis
412
+ # A NickAlreadyInUseError class. This exception is raised when Cereal
413
+ # tries to join an IRC server with a nickname (or nicknames) that is
414
+ # already in use.
415
+ #
416
+ # == Author
417
+ # Ross "Raws" Paffett
418
+ #
419
+ # == Copyright
420
+ # Copyright (c) 2009 Ross Paffett. Licensed under the MIT license:
421
+ # http://www.opensource.org/licenses/mit-license.php
422
+ class NickAlreadyInUseError < IrcError; end
423
+ end
@@ -0,0 +1,404 @@
1
+ module Cereal
2
+
3
+ # == Synopsis
4
+ # Cereal::Connection represents a connection to an IRC server. It
5
+ # is used internally by Cereal::Bot, and you never need to touch it
6
+ # to use Cereal. Each instance of Cereal::Bot has an attached
7
+ # Cereal::Connection instance that it communicates with the IRC
8
+ # server via.
9
+ #
10
+ # See the documentation for EventMachine::Connection for more detail.
11
+ #
12
+ # == Author
13
+ # Ross "Raws" Paffett
14
+ #
15
+ # == Copyright
16
+ # Copyright (c) 2009 Ross Paffett. Licensed under the MIT license:
17
+ # http://www.opensource.org/licenses/mit-license.php
18
+ class Connection < EventMachine::Connection
19
+
20
+ STATE_DISCONNECTED, STATE_CONNECTING, STATE_CONNECTED = 0, 1, 2
21
+
22
+ attr_reader :state, :channels
23
+ attr_accessor :temp_nick
24
+
25
+ def initialize(args)
26
+ # Our parent Cereal::Bot
27
+ @bot = args[:bot]
28
+
29
+ # Current state of this connection
30
+ @state = Cereal::Connection::STATE_DISCONNECTED
31
+
32
+ # Details about the server we last connected to
33
+ @host = nil
34
+ @password = args[:password]
35
+
36
+ # Hash of channels that is used to remember which users are
37
+ # in which channels
38
+ @channels = {}
39
+
40
+ # Hash to temporarily hold channel topics on RPL_TOPIC until
41
+ # we receive more information on RPL_TOPICINFO
42
+ @topics = {}
43
+
44
+ # A thread which is responsible for sending messages to the IRC
45
+ # server in a controlled manner. Its queue prevents a flood of
46
+ # messages from causing the bot to be kicked from a channel or server.
47
+ @output_queue = Queue.new
48
+ @output_thread = Thread.new do
49
+ while true
50
+ sleep @bot.message_delay
51
+ send_data(@output_queue.deq)
52
+ end
53
+ end
54
+
55
+ # A string buffer to hold incomplete lines (lines not ending in \r\n)
56
+ # that we receive from the server.
57
+ @incomplete_line_buffer = ''
58
+ end
59
+
60
+ # Send raw data to the IRC server immediately. A trailing newline
61
+ # is added automatically.
62
+ # [_data_] The data to send.
63
+ def send_data(data)
64
+ return if data.nil? || @state == Cereal::Connection::STATE_DISCONNECTED
65
+ super(data + "\r\n")
66
+ @bot.log(">>>#{data}")
67
+ end
68
+
69
+ # Send raw data to the IRC server through the output queue. Messages
70
+ # are sent every _n_ seconds, where _n_ is <code>@bot.message_delay</code>.
71
+ # To bypass the output queue, use send_data.
72
+ def send_raw(data)
73
+ @output_queue.enq(data)
74
+ end
75
+
76
+ # Called upon receiving one or more lines of data from the IRC server.
77
+ # handle_line is called for each received line of data.
78
+ # [_data_] The received data.
79
+ def receive_data(data)
80
+ data.each_line do |line|
81
+ if line[-2,2] != "\r\n"
82
+ @incomplete_line_buffer << line
83
+ else
84
+ line = @incomplete_line_buffer + line
85
+ @bot.log(line)
86
+ handle_line(line)
87
+ @incomplete_line_buffer = ''
88
+ end
89
+ end
90
+ end
91
+
92
+ # Called upon successfully establishing a socket connection to the IRC
93
+ # server. Sends initial messages to the server in an attempt to register
94
+ # a valid client connection.
95
+ def post_init
96
+ # Attempt to register connection with server
97
+ @state = Cereal::Connection::STATE_CONNECTING
98
+ send_data("PASS #{@password}") if !@password.nil?
99
+ bot_nick = @bot.nick
100
+ @temp_nick = 'CerealBot'
101
+ if bot_nick.is_a? Array
102
+ @temp_nick = bot_nick.shift
103
+ else
104
+ @temp_nick = bot_nick
105
+ end
106
+ send_data("NICK #{@temp_nick}")
107
+ send_data("USER #{@bot.login} 0 * :#{@bot.name}")
108
+ end
109
+
110
+ # Called whenever the connection is closed, whether intentional or not.
111
+ def unbind
112
+ @state = Cereal::Connection::STATE_DISCONNECTED
113
+ @bot.on_disconnect
114
+ end
115
+
116
+ private
117
+
118
+ # Handle one line of data received from the IRC server. This is probably a
119
+ # good method to look through if you are a) a masochist, b) a maintainer of
120
+ # this code or c) stark raving mad.
121
+ #
122
+ # Note that if you answer B, you are automatically qualified as both A and C.
123
+ def handle_line(line)
124
+ GC.start
125
+
126
+ # If this is a PING from the server, respond and return immediately
127
+ if line =~ /^PING /
128
+ @bot.on_server_ping(line[5..-1])
129
+ return
130
+ end
131
+
132
+ line.chomp!
133
+
134
+ if @state == Cereal::Connection::STATE_CONNECTING && line =~ /^:\S* (\d{3}) (\S+) (\S*).*$/
135
+ # We're in the process of connecting
136
+ code = $~[1].to_i
137
+ if code == 004
138
+ # We're connected to the server, joy
139
+ @state = Cereal::Connection::STATE_CONNECTED
140
+ @temp_nick = $~[2]
141
+ @host = $~[3]
142
+ @bot.confirm_nick(@temp_nick)
143
+ @bot.on_connect(@host)
144
+ elsif code == 433
145
+ # Some bellend is already using our nick
146
+ if @bot.nick.is_a?(Array) && !@bot.nick.empty?
147
+ @temp_nick = @bot.nick.shift
148
+ send_data("NICK #{@temp_nick}")
149
+ else
150
+ close_connection
151
+ raise IrcError, "Nickname(s) already in use."
152
+ end
153
+ elsif code >= 400 && code <= 599
154
+ # Error message of some sort
155
+ @state = Cereal::Connection::STATE_DISCONNECTED
156
+ close_connection
157
+ raise IrcError, "Could not log in to IRC server. (#{line})"
158
+ end
159
+ elsif @state == Cereal::Connection::STATE_CONNECTED
160
+ # We're already connected; handle normal commands
161
+ tokens = line.split
162
+ if tokens.size < 2
163
+ # We don't know what this line means
164
+ @bot.on_unknown(line)
165
+ return
166
+ end
167
+
168
+ sender = OpenStruct.new
169
+ sender.raw = tokens[0]
170
+ command = tokens[1] ? tokens[1] : ''
171
+ target = nil
172
+
173
+ if sender.raw =~ /^:(\S+)!(\S+)@(\S+)$/
174
+ # Sender is probably a human (hostmask)
175
+ sender.nick, sender.login, sender.hostname = $~[1], $~[2], $~[3]
176
+ elsif sender.raw =~ /^:\S+$/
177
+ # Message is, presumably, a server response
178
+ if tokens.size < 3
179
+ # Message must contain command and parameters; we don't know
180
+ # what this line means
181
+ @bot.on_unknown(line)
182
+ return
183
+ end
184
+
185
+ if command =~ /^\d+$/
186
+ # Command is a numeric server response
187
+ response = line[line.index(command, sender.raw.length)+4..-1]
188
+ command = command.to_i
189
+
190
+ if command == 322 && response =~ /([#&]\S+) (\d+) :(.*)/
191
+ # RPL_LIST: bit of information about a channel
192
+ channel = $~[1]
193
+ user_count = $~[2].to_i
194
+ topic = $~[3]
195
+ @bot.on_channel_info(channel, user_count, topic)
196
+ elsif command == 332 && response =~ /([#&]\S+) :(.*)/
197
+ # RPL_TOPIC: topic of channel we've just joined
198
+ channel = $~[1]
199
+ topic = $~[2]
200
+ @topics[channel] = topic
201
+ elsif command == 333 && response =~ /([#&]\S+) (\S+) (\S+)/
202
+ # RPL_TOPICINFO: more topic info of channel we've just joined
203
+ channel = $~[1]
204
+ topic = @topics.delete(channel)
205
+ return if topic.nil?
206
+ set_by = $~[2]
207
+ date = Time.at($~[3].to_i)
208
+ @bot.on_topic(set_by, channel, date, topic, false)
209
+ elsif command == 353 && response =~ /([=*@]) ([#&]\S+) :(.*)/
210
+ # RPL_NAMREPLY: list of nicks in a channel we've just joined
211
+ channel = $~[2]
212
+ nicks = $~[3].split
213
+ nicks.each do |str|
214
+ add_user(Cereal::User.new($~[1], $~[2]), channel) if str =~ /^([@+]{0,2})(\S+)$/
215
+ end
216
+ elsif command == 366 && response =~ /([#&]\S+) :End of NAMES list$/
217
+ # RPL_ENDOFNAMES: we've got the full list of users in a channel
218
+ # we've just joined
219
+ channel = $~[1]
220
+ users = @channels[channel]
221
+ @bot.on_user_list(channel, users)
222
+ end
223
+
224
+ @bot.on_server_response(command, response)
225
+
226
+ return
227
+ else
228
+ # This is not a server response; must be a nick without a login
229
+ # and hostname, or perhaps a NOTICE, etc. from the server
230
+ sender.nick = sender.raw
231
+ target = command
232
+ end
233
+ end
234
+
235
+ return if !command || !sender.nick
236
+
237
+ command.upcase!
238
+ sender.nick.slice!(0) if sender.nick[0,1] == ':'
239
+ target = tokens[2] if !target
240
+ target.slice!(0) if target[0,1] == ':'
241
+
242
+ # Check for CTCP requests
243
+ if command == 'PRIVMSG' && line.index(":\1") && line.index(":\1") > 0 && line[-1,1] == "\1"
244
+ request = line[line.index(":\1")+2..-2]
245
+
246
+ case
247
+ when request == "VERSION"
248
+ @bot.on_version(sender, target)
249
+ when request =~ /^ACTION/
250
+ @bot.on_action(sender, target, request[7..-1])
251
+ when request =~ /^PING/
252
+ @bot.on_ping(sender, target, request[5..-1])
253
+ when request == "TIME"
254
+ @bot.on_time(sender, target)
255
+ when request == "FINGER"
256
+ @bot.on_finger(sender, target)
257
+ when (tokens = request.split).size >= 5 && tokens[0] == "DCC"
258
+ # TODO Handle DCC requests -- for now, unknown line
259
+ @bot.on_unknown(line)
260
+ else
261
+ # Unknown CTCP message
262
+ @bot.on_unknown(line)
263
+ end
264
+ elsif command == 'PRIVMSG' && target =~ /^[#&].*$/
265
+ # Normal message to a channel
266
+ @bot.on_message(sender, target, line[line.index(' :')+2..-1])
267
+ elsif command == 'PRIVMSG'
268
+ # Private message to us
269
+ @bot.on_private_message(sender, line[line.index(' :')+2..-1])
270
+ elsif command == 'JOIN'
271
+ # Someone's joined a channel
272
+ add_user(sender.nick, target)
273
+ @bot.on_join(sender, target)
274
+ elsif command == 'PART'
275
+ # Someone's parted a channel
276
+ remove_user(sender.nick, target)
277
+ remove_channel(target) if sender.nick == @bot.nick
278
+ @bot.on_part(sender, target)
279
+ elsif command == 'NICK'
280
+ # Someone's changed their nick
281
+ rename_user(sender.nick, target)
282
+ # Update our nick if it was us that changed
283
+ @bot.confirm_nick(target) if sender.nick == @temp_nick
284
+ @bot.on_nick_change(sender, target)
285
+ elsif command == 'NOTICE'
286
+ # Someone is sending a notice
287
+ @bot.on_notice(sender, target, line[line.index(' :')+2..-1])
288
+ elsif command == 'QUIT'
289
+ @bot.on_quit(sender, line[line.index(' :')+2..-1])
290
+ # Someone's quit from the server
291
+ if sender.nick == @nick
292
+ remove_all_channels
293
+ else
294
+ remove_user(sender.nick)
295
+ end
296
+ elsif command == 'KICK'
297
+ # Someone's been kicked from a channel
298
+ recipient = tokens[3]
299
+ remove_channel(target) if recipient == @nick
300
+ remove_user(recipient, target)
301
+ @bot.on_kick(sender, target, recipient, line[line.index(' :')+2..-1])
302
+ elsif command == 'MODE'
303
+ # Someone's changed the mode on a channel or user
304
+ mode = line[line.index(target, 2)+target.length+1..-1]
305
+ mode.slice!(0) if mode[0,1] == ':'
306
+ process_mode(sender, target, mode)
307
+ elsif command == 'TOPIC'
308
+ # Someone's changed a channel's topic
309
+ @bot.on_topic(sender.nick, target, Time.new, line[line.index(' :')+2..-1], true)
310
+ elsif command == 'INVITE'
311
+ # Someone's inviting someone else to a channel
312
+ @bot.on_invite(sender, target, tokens[3])
313
+ end
314
+ end
315
+ end
316
+
317
+ # Called when the mode of a channel or user is set. Calls the
318
+ # appropriate on_op, on_deop, etc methods before handing the event
319
+ # over to @bot.on_mode.
320
+ def process_mode(sender, target, mode)
321
+ if target =~ /^[#&]/
322
+ # A channel's mode is being changed
323
+ # TODO Implement this
324
+ else
325
+ # A user's mode is being changed
326
+ # TODO Implement this, too
327
+ end
328
+ end
329
+
330
+ # Add a user to the specified channel we know about. Overwrite any
331
+ # existing entry.
332
+ def add_user(user, channel)
333
+ user = Cereal::User.new('', user.to_s) if !user.is_a? Cereal::User
334
+
335
+ channel.downcase!
336
+ users = @channels.fetch(channel) { |channel| @channels[channel] = {} }
337
+ users[user] = user
338
+ end
339
+
340
+ # Remove user from the specified channel or all channels we know about.
341
+ def remove_user(user, channel=nil)
342
+ user = Cereal::User.new('', user.to_s) if !user.is_a? Cereal::User
343
+
344
+ if channel
345
+ channel.downcase!
346
+ users = @channels.fetch(channel) { |channel| @channels[channel] = {} }
347
+ users.delete(user)
348
+ else
349
+ @channels.each { |users| users.delete(user) }
350
+ end
351
+ end
352
+
353
+ # Rename a user if they appear in any of our known channels.
354
+ def rename_user(old_nick, new_nick)
355
+ old_user = Cereal::User.new('', old_nick)
356
+ @channels.each do |channel,users|
357
+ user = users[old_user]
358
+ user.nick = new_nick if user
359
+ end
360
+ end
361
+
362
+ # Change a user's mode (prefix). Valid values for +mode_change+ include:
363
+ # ["+@+"] add op and voice
364
+ # ["+@"] add op
365
+ # ["++"] add voice
366
+ # ["-+"] remove voice
367
+ # ["-@"] remove op
368
+ # ["-@+"] remove op and voice
369
+ def change_user_mode(nick, channel, mode_change)
370
+ channel.downcase!
371
+ users = @channels.fetch(channel) { |channel| @channels[channel] = {} }
372
+ user = users[Cereal::User.new('', nick)]
373
+ if user
374
+ case mode_change
375
+ when '+@+'
376
+ user.op(true)
377
+ user.voice(true)
378
+ when '+@'
379
+ user.op(true)
380
+ when '++'
381
+ user.voice(true)
382
+ when '-+'
383
+ user.voice(false)
384
+ when '-@'
385
+ user.op(false)
386
+ when '-@+'
387
+ user.op(false)
388
+ user.voice(false)
389
+ end
390
+ end
391
+ end
392
+
393
+ # Remove a channel from our known channels.
394
+ def remove_channel(channel)
395
+ @channels.delete(channel.downcase)
396
+ end
397
+
398
+ # Clear all known channels.
399
+ def remove_all_channels
400
+ @channels = {}
401
+ end
402
+ end
403
+
404
+ end
@@ -0,0 +1,98 @@
1
+ module Cereal
2
+
3
+ # == Synopsis
4
+ # Cereal::User Represents a user in an IRC channel. This class is
5
+ # used by Cereal::Connection and Cereal::Bot to keep track of who is
6
+ # in each channel the bot currenty occupies. You may receive an array
7
+ # of Users if you request a list of users in a channel from the bot.
8
+ #
9
+ # <code>@lower_nick</code> is set on initialization (or when
10
+ # <code>@nick</code> is changed) and is simply a <code>downcase</code>d
11
+ # version of <code>@nick</code>, stored as an instance variable for
12
+ # performance reasons.
13
+ #
14
+ # == Author
15
+ # Ross "Raws" Paffett
16
+ #
17
+ # == Copyright
18
+ # Copyright (c) 2009 Ross Paffett. Licensed under the MIT license:
19
+ # http://www.opensource.org/licenses/mit-license.php
20
+ class User
21
+
22
+ include Comparable
23
+
24
+ attr_reader :prefix, :nick, :lower_nick
25
+
26
+ # Constructs a User object with a known prefix and nick.
27
+ def initialize(prefix, nick)
28
+ @prefix = prefix.strip
29
+ @nick = nick.strip
30
+ @lower_nick = @nick.downcase
31
+ end
32
+
33
+ # Returns whether or not this user is an operator.
34
+ def op?
35
+ !@prefix.index('@').nil?
36
+ end
37
+
38
+ # Sets whether or not this user is an operator.
39
+ def op(is_op)
40
+ if is_op
41
+ @prefix = voice? ? '@+' : '@'
42
+ else
43
+ @prefix = voice? ? '+' : ''
44
+ end
45
+ end
46
+
47
+ # Returns whether or not this user has voice.
48
+ def voice?
49
+ !@prefix.index('+').nil?
50
+ end
51
+
52
+ # Sets whether or not this user has voice.
53
+ def voice(has_voice)
54
+ if has_voice
55
+ @prefix = op? ? '@+' : '+'
56
+ else
57
+ @prefix = op? ? '@' : ''
58
+ end
59
+ end
60
+
61
+ # Returns the nick of the user with their prefix,
62
+ # if any, attached.
63
+ #
64
+ # u = Cereal::User.new('+', 'Monty')
65
+ # u.to_s => "+Monty"
66
+ def to_s
67
+ @prefix + @nick
68
+ end
69
+
70
+ # Set this user's nick.
71
+ def nick=(new_nick)
72
+ @nick = new_nick.strip
73
+ @lower_nick = @nick.downcase
74
+ end
75
+
76
+ # Returns true as in ==.
77
+ def eql?(other)
78
+ self == other
79
+ end
80
+
81
+ # Returns the hash of this User object.
82
+ def hash
83
+ @lower_nick.hash
84
+ end
85
+
86
+ # Returns the result of calling <=> on lowercased nicks.
87
+ # This is useful for sorting lists of Users.
88
+ def <=>(other)
89
+ if other.is_a? Cereal::User
90
+ other.lower_nick <=> @lower_nick
91
+ else
92
+ other.to_s.downcase! <=> @lower_nick
93
+ end
94
+ end
95
+
96
+ end
97
+
98
+ end
metadata ADDED
@@ -0,0 +1,71 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cereal
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.8
5
+ platform: ruby
6
+ authors:
7
+ - Ross Paffett
8
+ - Jon R.
9
+ - Brian S
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+
14
+ date: 2009-04-01 15:51:42 -04:00
15
+ default_executable:
16
+ dependencies:
17
+ - !ruby/object:Gem::Dependency
18
+ name: eventmachine
19
+ type: :runtime
20
+ version_requirement:
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: 0.12.2
26
+ version:
27
+ description: Cereal provides an event-based IRC connection framework built on EventMachine and inspired by PircBot.
28
+ email: nobody@inparticul.ar
29
+ executables: []
30
+
31
+ extensions: []
32
+
33
+ extra_rdoc_files: []
34
+
35
+ files:
36
+ - lib/cereal.rb
37
+ - lib/connection.rb
38
+ - lib/user.rb
39
+ - CHANGES
40
+ - MIT-LICENSE
41
+ - Rakefile
42
+ - README
43
+ - TODO
44
+ has_rdoc: false
45
+ homepage: http://cerealbot.org/
46
+ post_install_message:
47
+ rdoc_options: []
48
+
49
+ require_paths:
50
+ - lib
51
+ required_ruby_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: "0"
56
+ version:
57
+ required_rubygems_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: "0"
62
+ version:
63
+ requirements: []
64
+
65
+ rubyforge_project:
66
+ rubygems_version: 1.3.1
67
+ signing_key:
68
+ specification_version: 2
69
+ summary: Event-based IRC connection framework.
70
+ test_files: []
71
+