cereal 0.1.8

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 (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
+