net-yail 1.0.0

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.
@@ -0,0 +1,132 @@
1
+ require 'rubygems'
2
+ gem 'net-yail'
3
+
4
+ # My abstraction from adapter to a real bot.
5
+ #
6
+ # At the moment this only supports single-channel joining. It's just a very
7
+ # basic example, kids. Deal with it.
8
+ class IRCBot
9
+ attr_reader :irc
10
+
11
+ public
12
+
13
+ # Creates a new bot yay. Note that due to my laziness, the options here
14
+ # are almost exactly the same as those in Net::YAIL. But at least there
15
+ # are more defaults here.
16
+ #
17
+ # Options:
18
+ # * <tt>:irc_network</tt>: Name/IP of the IRC server
19
+ # * <tt>:channel</tt>: Channel name to join
20
+ # * <tt>:port</tt>: Port number, defaults to 6667
21
+ # * <tt>:username</tt>: Username reported to server
22
+ # * <tt>:realname</tt>: Real name reported to server
23
+ # * <tt>:nicknames</tt>: Array of nicknames to cycle through
24
+ # * <tt>:silent</tt>: Silence a lot of reports
25
+ # * <tt>:loud</tt>: Lots more verbose reports
26
+ def initialize(options = {})
27
+ @channel = options[:channel]
28
+ @irc_network = options[:irc_network]
29
+ @port = options[:port] || 6667
30
+ @username = options[:username] || 'IRCBot'
31
+ @realname = options[:realname] || 'IRCBot'
32
+ @nicknames = options[:nicknames] || ['IRCBot1', 'IRCBot2', 'IRCBot3']
33
+ @silent = options[:silent] || false
34
+ @loud = options[:loud] || false
35
+ end
36
+
37
+ # Creates the socket connection and registers the (very simple) default
38
+ # welcome handler. Subclasses should build their hooks in
39
+ # add_custom_handlers to allow auto-creation in case of a restart.
40
+ def connect_socket
41
+ @irc = Net::YAIL.new(
42
+ :address => @irc_network,
43
+ :port => @port,
44
+ :username => @username,
45
+ :realname => @realname,
46
+ :nicknames => @nicknames,
47
+ :silent => @silent,
48
+ :loud => @loud
49
+ )
50
+
51
+ # Simple hook for welcome to allow auto-joining of the channel
52
+ @irc.prepend_handler :incoming_welcome, self.method(:welcome)
53
+
54
+ add_custom_handlers
55
+ end
56
+
57
+ # To be subclassed - this method is a nice central location to allow the
58
+ # bot to register its handlers before this class takes control and hits
59
+ # the IRC network.
60
+ def add_custom_handlers
61
+ raise "You must define your handlers in add_custom_handlers, or else " +
62
+ "explicitly override with an empty method."
63
+ end
64
+
65
+ # Enters the socket's listening loop(s)
66
+ def start_listening
67
+ @irc.start_listening
68
+ end
69
+
70
+ # Tells us the main app wants to just wait until we're done with all
71
+ # thread processing, or get a kill signal, or whatever. For now this is
72
+ # basically an endless loop that lets the threads do their thing until
73
+ # the socket dies. If a bot wants, it can handle :irc_loop to do regular
74
+ # processing.
75
+ def irc_loop
76
+ while true
77
+ until @irc.dead_socket || @irc.socket.eof?
78
+ sleep 15
79
+ @irc.handle(:irc_loop)
80
+ Thread.pass
81
+ end
82
+
83
+ # Disconnected? Wait a little while and start up again.
84
+ sleep 30
85
+ @irc.stop_listening
86
+ self.connect_socket
87
+ start_listening
88
+ end
89
+ end
90
+
91
+ private
92
+ # Basic handler for joining a single channel upon successful registration
93
+ def welcome
94
+ @irc.join(@channel)
95
+ # Let the default welcome stuff still happen
96
+ return false
97
+ end
98
+
99
+ ################
100
+ # Helpful wrappers
101
+ ################
102
+
103
+ # Wraps Net::YAIL.me
104
+ def bot_name
105
+ @irc.me
106
+ end
107
+
108
+ # Wraps Net::YAIL.msg
109
+ def msg(*args)
110
+ @irc.msg(*args)
111
+ end
112
+
113
+ # Wraps Net::YAIL.act
114
+ def act(*args)
115
+ @irc.act(*args)
116
+ end
117
+
118
+ # Wraps Net::YAIL.join
119
+ def join(*args)
120
+ @irc.join(*args)
121
+ end
122
+
123
+ # Wraps Net::YAIL.report
124
+ def report(*args)
125
+ @irc.report(*args)
126
+ end
127
+
128
+ # Wraps Net::YAIL.nick
129
+ def nick(*args)
130
+ @irc.nick(*args)
131
+ end
132
+ end
data/README ADDED
@@ -0,0 +1,66 @@
1
+ Rubyforge page: http://rubyforge.org/projects/ruby-irc-yail
2
+
3
+ Net::YAIL is a library built for dealing with IRC communications in Ruby.
4
+ This is a project I've been building for about three years, based
5
+ originally on the very messy initial release of IRCSocket (back when I first
6
+ started, that was the only halfway-decent IRC lib I found). I've put a lot
7
+ of time and effort into cleaning it up to make it better for my own uses,
8
+ and now it's almost entirely my code.
9
+
10
+ Some credit should also be given to Ruby-IRC, as I stole its eventmap.yml
11
+ file with very minor modifications.
12
+
13
+ This library may not be useful to everybody (or anybody other than myself,
14
+ for that matter), and Ruby-IRC or another lib may work for your situation
15
+ far better than this thing will, but the general design I built here has
16
+ just felt more natural to me than the other libraries I've looked at since
17
+ I started my project.
18
+
19
+ Features of YAIL:
20
+
21
+ * Allows event handlers to be specified very easily for all known IRC events,
22
+ and except in a few rare cases one can choose to override the default
23
+ handling mechanisms.
24
+ * Allows handling outgoing messages, such as when privmsg is called. The API
25
+ won't allow you to stop the outgoing message (though I may offer this if
26
+ people want it), but you can filter data before it's sent out. This is one
27
+ thing I didn't see anywhere else.
28
+ * Threads for input and output are persistent. This is a feature, not a bug.
29
+ Some may hate this approach, but I'm a total n00b to threads, and it seemed
30
+ like the way to go, having thread loops responsible for their own piece of
31
+ the library. I'd *love* input here if anybody can tell me why this is a bad
32
+ idea....
33
+ * "Stacked" event handling is possible if you want to provide a very modular
34
+ framework of your own. When you prepend a handler, its return determines if
35
+ the next handler will get called. This isn't useful for a simple bot most
36
+ likely, but can have some utility in bigger projects where a single event
37
+ may need to be dispatched to several handlers.
38
+ * Easy to build a simple bot without subclassing anything. One gripe I had
39
+ with IRCSocket was that it was painful to do anything without subclassing
40
+ and overriding methods. No need here.
41
+ * Lots of built-in reporting. You may hate this part, but for a bot, it's
42
+ really handy to have most incoming data reported on some level. I may make
43
+ this optional at some point, but only if people complain, since I haven't
44
+ yet seen a need to do so....
45
+ * Built-in PRIVMSG buffering! You can of course choose to not buffer, but by
46
+ default you cannot send more than one message to a given target (user or
47
+ channel) more than once per second. Additionally, this buffering method is
48
+ ideal for a bot that's trying to be chatty on two channels at once, because
49
+ buffering is per-target, so queing up 20 lines on <tt>##foo</tt> doesn't mean waiting
50
+ 20 seconds to spit data out to <tt>##bar</tt>. The one caveat here is that if your
51
+ app is trying to talk to too many targets at once, the buffering still won't
52
+ save you from a flood-related server kick. If this is a problem for others,
53
+ I'll look into building an even more awesome buffering system.
54
+ * The included IRCBot is a great starting point for building your own bot,
55
+ but if you want something even simpler, just look at Net::YAIL's documentation
56
+ for the most basic working examples.
57
+
58
+ I still have a lot to do, though. The output API is definitely not fully
59
+ fleshed out - there are many commands I haven't yet built a shortcut for, such
60
+ as KICK or TOPIC. I believe that the library is also missing a lot for people
61
+ who just have a different approach than me, since this was purely designed for
62
+ my own benefit, and then released almost exclusively to piss off the people
63
+ whose work I stole to get where I'm at today. (Just kiddin', Pope)
64
+
65
+ This code is released under the MIT license. I hear it's all the rage with
66
+ the kids these days.
@@ -0,0 +1,525 @@
1
+ require 'socket'
2
+ require 'thread'
3
+ require 'yaml'
4
+
5
+ # To make this library seem smaller, a lot of code has been split up and put
6
+ # into semi-logical files. I don't really like this hacky solution, but I
7
+ # cannot figure out a nicer way to keep the code as clean as I like.
8
+ require 'net/yail/magic_events'
9
+ require 'net/yail/default_events'
10
+ require 'net/yail/output_api'
11
+
12
+ # TODO:
13
+ # * Extract all pattern matching into an external file - store both the
14
+ # pattern and the event handle's symbol.
15
+ # * Build a system to allow numeric events to get sent post-processed data
16
+ # if it makes sense (converting the text to specific parts instead of all
17
+ # handlers having to regex it themselves, for instance)
18
+
19
+ # If a thread crashes, I want the app to die. My threads are persistent, not
20
+ # temporary.
21
+ Thread.abort_on_exception = true
22
+
23
+ module Net
24
+
25
+ # This library is based on the initial release of IRCSocket with a tiny bit
26
+ # of plagarism of Ruby-IRC.
27
+ #
28
+ # My aim here is to build something that is still fairly simple to use, but
29
+ # powerful enough to build a decent IRC program.
30
+ #
31
+ # This is far from complete, but it does successfully power a relatively
32
+ # complicated bot, so I believe it's solid and "good enough" for basic tasks.
33
+ #
34
+ # =Events
35
+ #
36
+ # * Register handlers by calling prepend_handler(symbol, method)
37
+ # * Events based on incoming data are represented by :incoming_*, while
38
+ # outgoing are :outgoing_*
39
+ # * I'm still using the names from IRCSocket dev(s), so this means an incoming
40
+ # message would call the :incoming_msg handler, and a message being sent
41
+ # would call the :outgoing_msg handler.
42
+ #
43
+ # ==Incoming Events
44
+ #
45
+ # Current list of incoming events and the parameters sent to the handler:
46
+ # * :incoming_msg(fullactor, actor, target, text) - Normal message from actor to target
47
+ # * :incoming_act(fullactor, actor, target, text) - CTCP "action" (emote) from actor to target
48
+ # * :incoming_ctcp(fullactor, actor, target, text) - CTCP other than "action" from actor to target
49
+ # * :incoming_ctcpreply(fullactor, actor, target, text) - CTCP NOTICE from actor to target
50
+ # * :incoming_notice(fullactor, actor, target, text) - other NOTICE from actor to target
51
+ # * :incoming_mode(fullactor, actor, target, modes, objects) - actor sets modes on objects in target channel
52
+ # * :incoming_join(fullactor, actor, target) - actor joins target channel
53
+ # * :incoming_part(fullactor, actor, target, text) - actor leaves target with message in text
54
+ # * :incoming_kick(fullactor, actor, target, object, text) - actor kicked object from target with reason 'text'
55
+ # * :incoming_quit(fullactor, actor, text) - actor left server completely with reason 'text'
56
+ # * :incoming_nick(fullactor, actor, nickname) - actor changed to nickname
57
+ # * :incoming_ping(text) - ping from server with given text
58
+ # * :incoming_miscellany(line) - text from server didn't match anything known
59
+ # * :incoming_welcome(text, args) - raw 001 from server, means we successfully logged in
60
+ # * :incoming_bannedfromchan(text, args) - banned from channel
61
+ # * Anything else in the eventmap.yml file with params(text, args).
62
+ #
63
+ # Common parameter elements:
64
+ # * fullactor: Rarely needed, full text of origin of an action
65
+ # * actor: Nickname of originator of an action
66
+ # * target: Nickname for private actions, channel name for public
67
+ # * text: Actual message/emote/notice/etc
68
+ # * args: For numeric handlers, this is a hash of :fullactor, :actor, and
69
+ # :target. Most numeric handlers I've built don't need this, so I made it
70
+ # easier to just get what you specifically want.
71
+ #
72
+ # ==Outgoing Events
73
+ #
74
+ # Generally speaking, you won't need these very often, but they're here for
75
+ # the edge cases all the same. Note that the socket output cannot be skipped
76
+ # (see Return value from events below), so this is truly just to allow
77
+ # modifying things before they go out (filtering speech, converting or
78
+ # stripping markup, etc) or just general stats-type logic.
79
+ #
80
+ # Events:
81
+ # * :outgoing_begin_connection(username, address, realname) - called when the
82
+ # start_listening method has set up all threading and such. Default behavior
83
+ # is to call user() and nick()
84
+ # * :outgoing_privmsg(target, text) - Any kind of PRIVMSG output is about to
85
+ # get sent out
86
+ # * :outgoing_msg(target, text) - Hit by a direct call to msg, which is
87
+ # normally used for "plain" messages, but a "clever" user could do their own
88
+ # CTCP messages here as well. Shoot them if they do.
89
+ # * :outgoing_ctcp(target, text) - All CTCP messages hit here eventually
90
+ # * :outgoing_act(target, text) - ACTION CTCP messages should go through this,
91
+ # not manually use ctcp.
92
+ # * :outgoing_notice(target, text) - All NOTICE messages hit here
93
+ # * :outgoing_ctcpreply(target, text) - CTCP NOTICE messages
94
+ # * :outgoing_mode(target, modes, objects) - Sets or queries mode. If modes is
95
+ # present, sends mode list to target. Objects would be users.
96
+ # * :outgoing_join(target) - The given target channel is about to be joined
97
+ # * :outgoing_part(target, text) - The given target channel is about to be
98
+ # left, with optional text reason.
99
+ # * :outgoing_quit(text) - The client is about to quit, with optional text
100
+ # reason.
101
+ # * :outgoing_nick(new_nick) - The client is about to change nickname
102
+ # * :outgoing_user(username, myaddress, address, realname) - We're about to
103
+ # send a USER command.
104
+ #
105
+ # Note that a single output call can hit multiple handlers, so you must plan
106
+ # carefully. A call to act() will hit the act handler, then ctcp (since act
107
+ # is a type of ctcp message), then privmsg.
108
+ #
109
+ # ==Custom Events
110
+ #
111
+ # Yes, you can register your own wacky event handlers if you like, and have
112
+ # your code call them. Just register a handler with some funky name of
113
+ # your own design (avoid the prefixes :incoming and :outgoing for obvious
114
+ # reasons), and so long as something calls that handler, your handler method
115
+ # will get its data.
116
+ #
117
+ # This isn't likely useful for a simple program, but for a subclass or wrapper
118
+ # of the IRC class, having the ability to give *its* users new events without
119
+ # mucking up this class can be helpful. For instance, see IRCBot#irc_loop
120
+ # and the :irc_loop event. If one wants their bot to do something regularly,
121
+ # they just handle that event and get frequent calls.
122
+ #
123
+ # ==Return value from events
124
+ #
125
+ # The return can be *critical* - a true value tells the handlers to stop
126
+ # their chain (true = "yes, I handled this event, stay the frak away you
127
+ # other, lesser handlers!), so no other handlers will be called.
128
+ #
129
+ # Note that critical handlers (incoming ping, welcome, and nick change) cannot
130
+ # be overwritten as they actually run *before* user-defined handlers, and
131
+ # output handlers are just for filtering and cannot stop the socket from
132
+ # sending its data. If you want to change that low-level stuff, you should
133
+ # subclass, modify the code directly, monkey-patch, or just write your own
134
+ # library.
135
+ #
136
+ # When should you return false from an event? Generally any time you have a
137
+ # handler that really needs to report itself. Unless you have multiple
138
+ # layers of handlers for a given event, there's little reason to worry about
139
+ # breaking the chain of events. Since handlers are *prepended* to the list,
140
+ # anybody subclassing your code can override your events, not the other way
141
+ # around. The main use is if you have multiple handlers for a single complex
142
+ # event, where you want each handler to do its own set process and pass on the
143
+ # event if it isn't resposible for that particular situation. Allows complex
144
+ # interactions to be made a bit cleaner, theoretically.
145
+ #
146
+ # =Simple example
147
+ #
148
+ # For a program to do anything useful, it must instantiate an object with
149
+ # useful data and register some handlers:
150
+ #
151
+ # require 'rubygems'
152
+ # require 'net/yail'
153
+ #
154
+ # irc = Net::YAIL.new(
155
+ # :address => 'irc.someplace.co.uk',
156
+ # :username => 'Frakking Bot',
157
+ # :realname => 'John Botfrakker',
158
+ # :nicknames => ['bot1', 'bot2', 'bot3']
159
+ # )
160
+ #
161
+ # irc.prepend_handler :incoming_welcome, proc {
162
+ # irc.join('#foo')
163
+ # return false
164
+ # }
165
+ #
166
+ # irc.start_listening
167
+ # while irc.dead_socket == false
168
+ # # Avoid major CPU overuse by taking a very short nap
169
+ # sleep 0.05
170
+ # end
171
+ #
172
+ # Now we've built a simple IRC listener that will connect to a (probably
173
+ # invalid) network, identify itself, and sit around waiting for the welcome
174
+ # message. After this has occurred, we join a channel and return false.
175
+ #
176
+ # One could also define a method instead of a proc:
177
+ #
178
+ # require 'rubygems'
179
+ # require 'net/yail'
180
+ #
181
+ # def welcome
182
+ # @irc.join('#channel')
183
+ # return false
184
+ # end
185
+ #
186
+ # irc = Net::YAIL.new(
187
+ # :address => 'irc.someplace.co.uk',
188
+ # :username => 'Frakking Bot',
189
+ # :realname => 'John Botfrakker',
190
+ # :nicknames => ['bot1', 'bot2', 'bot3']
191
+ # )
192
+ #
193
+ # irc.prepend_handler :incoming_welcome, method(:welcome)
194
+ # irc.start_listening
195
+ # while irc.dead_socket == false
196
+ # # Avoid major CPU overuse by taking a very short nap
197
+ # sleep 0.05
198
+ # end
199
+ #
200
+ # =Better example
201
+ #
202
+ # See the included IRCBot for a basic bot base class example.
203
+ class YAIL
204
+ include Net::IRCEvents::Magic
205
+ include Net::IRCEvents::Defaults
206
+ include Net::IRCOutputAPI
207
+
208
+ attr_reader(
209
+ :me, # Nickname on the IRC server
210
+ :registered, # If true, we've been welcomed
211
+ :nicknames, # Array of nicknames to try when logging on to server
212
+ :dead_socket, # True if we have no data after a read operation
213
+ :socket # TCPSocket instance
214
+ )
215
+ attr_accessor(
216
+ :silent,
217
+ :loud
218
+ )
219
+
220
+ # Makes a new instance, obviously.
221
+ #
222
+ # Note: I haven't done this everywhere, but for the constructor, I felt
223
+ # it needed to have hash-based args. It's just cleaner to me when you're
224
+ # taking this many args.
225
+ #
226
+ # Options:
227
+ # * <tt>:address</tt>: Name/IP of the IRC server
228
+ # * <tt>:port</tt>: Port number, defaults to 6667
229
+ # * <tt>:username</tt>: Username reported to server
230
+ # * <tt>:realname</tt>: Real name reported to server
231
+ # * <tt>:nicknames</tt>: Array of nicknames to cycle through
232
+ # * <tt>:silent</tt>: Don't report output messages from this object,
233
+ # defaults to false
234
+ # * <tt>:loud</tt>: Report a whole lot of stuff that's normally silenced and
235
+ # is generally very annoying. Defaults to false, thankfully.
236
+ # * <tt>:throttle_seconds</tt>: Seconds between a cycle of privmsg sends.
237
+ # Defaults to 1. One "cycle" is defined as sending one line of output to
238
+ # *all* targets that have output buffered.
239
+ def initialize(options = {})
240
+ @me = ''
241
+ @nicknames = options[:nicknames]
242
+ @registered = false
243
+ @username = options[:username]
244
+ @realname = options[:realname]
245
+ @address = options[:address]
246
+ @port = options[:port] || 6667
247
+ @silent = options[:silent] || false
248
+ @loud = options[:loud] || false
249
+ @throttle_seconds = options[:throttle_seconds] || 1
250
+
251
+ # Read in map of event numbers and names. Yes, I stole this event map
252
+ # file from RubyIRC and made very minor changes.... They stole it from
253
+ # somewhere else anyway, so it's okay.
254
+ eventmap = "#{File.dirname(__FILE__)}/yail/eventmap.yml"
255
+ @event_number_lookup = File.open(eventmap) { |file| YAML::load(file) }.invert
256
+
257
+ # Build our socket
258
+ @socket = TCPSocket.new(@address, @port)
259
+
260
+ # We're not dead... yet...
261
+ @dead_socket = false
262
+
263
+ # Shared resources for threads to try and coordinate.... I know very
264
+ # little about thread safety, so this stuff may be a terrible disaster.
265
+ # Please send me better approaches if you are less stupid than I.
266
+ @input_buffer = []
267
+ @input_buffer_mutex = Mutex.new
268
+ @privmsg_buffer = {}
269
+ @privmsg_buffer_mutex = Mutex.new
270
+
271
+ # Buffered output is allowed to go out right away.
272
+ @next_message_time = Time.now
273
+
274
+ # Setup handlers
275
+ @handlers = Hash.new
276
+ setup_default_handlers
277
+ end
278
+
279
+ # Starts listening for input and builds the perma-threads that check for
280
+ # input, output, and privmsg buffering.
281
+ def start_listening
282
+ # We don't want to spawn an extra listener
283
+ return if Thread === @ioloop_thread
284
+
285
+ # Build forced / magic logic - welcome setting @me, ping response, etc.
286
+ # Since we do these here, nobody can skip them and they're always first.
287
+ setup_magic_handlers
288
+
289
+ # Begin the listening thread
290
+ @ioloop_thread = Thread.new {io_loop}
291
+ @input_processor = Thread.new {process_input_loop}
292
+ @privmsg_processor = Thread.new {process_privmsg_loop}
293
+
294
+ # Let's begin the cycle by telling the server who we are. This should
295
+ # start a TERRIBLE CHAIN OF EVENTS!!!
296
+ handle(:outgoing_begin_connection, @username, @address, @realname)
297
+ end
298
+
299
+ # Kills and clears all threads. See note above about my lack of knowledge
300
+ # regarding threads. Please help me if you know how to make this system
301
+ # better. DEAR LORD HELP ME IF YOU CAN!
302
+ def stop_listening
303
+ @ioloop_thread.terminate
304
+ @input_processor.terminate
305
+ @privmsg_processor.terminate
306
+
307
+ @ioloop_thread = nil
308
+ @input_processor = nil
309
+ @privmsg_processor = nil
310
+ end
311
+
312
+ private
313
+
314
+ # Reads incoming data - should only be called by io_loop, and only when
315
+ # we've already ensured that data is, in fact, available.
316
+ def read_incoming_data
317
+ line = @socket.gets
318
+
319
+ # If we have no data, the socket closed. Nothing to do but set a
320
+ # status and return
321
+ if !line
322
+ @dead_socket = true
323
+ return
324
+ end
325
+
326
+ line.chomp!
327
+
328
+ report "+++INCOMING: #{line}" if @loud
329
+
330
+ # Only synchronize long enough to push our incoming string onto the
331
+ # input buffer
332
+ @input_buffer_mutex.synchronize do
333
+ @input_buffer.push(line)
334
+ end
335
+ end
336
+
337
+ # This should be called from a thread only! Does nothing but listens
338
+ # forever for incoming data, and calling handlers due to this listening
339
+ def io_loop
340
+ while true
341
+ # if no data is coming in, don't block the socket!
342
+ read_incoming_data if Kernel.select([@socket], nil, nil, 0)
343
+ sleep 0.05
344
+ end
345
+ end
346
+
347
+ # This again is a thread-only method. Loops forever, handling input
348
+ # whenever the @input_buffer var has any.
349
+ def process_input_loop
350
+ lines = nil
351
+ while true
352
+ # Only synchronize long enough to copy and clear the input buffer.
353
+ @input_buffer_mutex.synchronize do
354
+ lines = @input_buffer.dup
355
+ @input_buffer.clear
356
+ end
357
+
358
+ if (lines)
359
+ # Now actually handle the data we copied, secure in the knowledge
360
+ # that our reader thread is no longer going to wait on us.
361
+ while lines.empty? == false
362
+ process_input(lines.shift)
363
+ end
364
+
365
+ lines = nil
366
+ end
367
+
368
+ sleep 0.05
369
+ end
370
+ end
371
+
372
+ # Grabs one message for each target in the private message buffer, removing
373
+ # messages from @privmsg_buffer. Returns a hash array of target -> text
374
+ def pop_privmsgs
375
+ privmsgs = {}
376
+
377
+ # Only synchronize long enough to pop the appropriate messages. By
378
+ # the way, this is UGLY! I should really move some of this stuff....
379
+ @privmsg_buffer_mutex.synchronize do
380
+ for target in @privmsg_buffer.keys
381
+ # Clean up our buffer to avoid a bunch of empty elements wasting
382
+ # time and space
383
+ if @privmsg_buffer[target].nil? || @privmsg_buffer[target].empty?
384
+ @privmsg_buffer.delete(target)
385
+ next
386
+ end
387
+
388
+ privmsgs[target] = @privmsg_buffer[target].shift
389
+ end
390
+ end
391
+
392
+ return privmsgs
393
+ end
394
+
395
+ # Checks for new private messages, and outputs all that are gathered from
396
+ # pop_privmsgs, if any
397
+ def check_privmsg_output
398
+ privmsgs = pop_privmsgs
399
+ @next_message_time = Time.now + @throttle_seconds unless privmsgs.empty?
400
+
401
+ for (target, out_array) in privmsgs
402
+ report(out_array[1]) unless out_array[1].to_s.empty?
403
+ raw("PRIVMSG #{target} :#{out_array.first}", false)
404
+ end
405
+ end
406
+
407
+ # Our final thread loop - grabs the first privmsg for each target and
408
+ # sends it on its way.
409
+ def process_privmsg_loop
410
+ while true
411
+ check_privmsg_output if @next_message_time <= Time.now && !@privmsg_buffer.empty?
412
+
413
+ sleep 0.05
414
+ end
415
+ end
416
+
417
+ # Gets some input, sends stuff off to a handler. Yay.
418
+ def process_input(line)
419
+ case line
420
+ when /^:((.+?)(?:!.+?)?) PRIVMSG (\S+?) :?\001ACTION (.+?)\001$/i
421
+ handle :incoming_act, $1, $2, $3, $4
422
+ when /^:((.+?)(?:!.+?)?) PRIVMSG (\S+?) :?\001(.+?)\001$/i
423
+ handle :incoming_ctcp, $1, $2, $3, $4
424
+ when /^:((.+?)(?:!.+?)?) PRIVMSG (\S+?) :?(.+?)$/i
425
+ handle :incoming_msg, $1, $2, $3, $4
426
+ when /^:((.+?)(?:!.+?)?) NOTICE (\S+?) :?\001(.+?)\001$/i
427
+ handle :incoming_ctcpreply, $1, $2, $3, $4
428
+ when /^:((.+?)(?:!.+?)?) NOTICE (\S+?) :?(.+?)$/i
429
+ handle :incoming_notice, $1, $2, $3, $4
430
+ when /^:((.+?)(?:!.+?)?) MODE (\S+?) :?(\S+?)(?: (.+?))?$/i
431
+ handle :incoming_mode, $1, $2, $3, $4, $5
432
+ when /^:((.+?)(?:!.+?)?) JOIN :?(\S+?)$/i
433
+ handle :incoming_join, $1, $2, $3
434
+ when /^:((.+?)(?:!.+?)?) PART (\S+?)(?: :?(\S+?)?)?$/i
435
+ handle :incoming_part, $1, $2, $3, $4
436
+ when /^:((.+?)(?:!.+?)?) KICK (\S+?) (\S+?) :?(.+?)$/i
437
+ handle :incoming_kick, $1, $2, $3, $4, $5
438
+ when /^:((.+?)(?:!.+?)?) QUIT :?(.+?)$/i
439
+ handle :incoming_quit, $1, $2, $3
440
+ when /^:((.+?)(?:!.+?)?) NICK :?(\S+?)$/i
441
+ handle :incoming_nick, $1, $2, $3
442
+ when /^PING :?(.+?)$/i
443
+ handle :incoming_ping, $1
444
+ when /^:((.+?)(?:!.+?)?) (\d{3})\s+(\S+?) (.+?)$/i
445
+ handle_numeric($3.to_i, $1, $2, $4, $5)
446
+ else
447
+ handle :incoming_miscellany, line
448
+ end
449
+ end
450
+
451
+ ##################################################
452
+ # EVENT HANDLING ULTRA SUPERSYSTEM DELUXE!!!
453
+ ##################################################
454
+
455
+ public
456
+ # Event handler hook. Kinda hacky. Calls your event(s) before the default
457
+ # event. Default stuff will happen if your handler doesn't return true.
458
+ def prepend_handler(event, *procs)
459
+ raise "Cannot change handlers while threads are listening!" if @ioloop_thread
460
+
461
+ # See if this is a word for a numeric - only applies to incoming events
462
+ if (event.to_s =~ /^incoming_(.*)$/)
463
+ number = @event_number_lookup[$1].to_i
464
+ event = :"incoming_numeric_#{number}" if number > 0
465
+ end
466
+
467
+ @handlers[event] ||= Array.new
468
+ until procs.empty?
469
+ @handlers[event].unshift(procs.pop)
470
+ end
471
+ end
472
+
473
+ # Handles the given event (if it's in the @handlers array) with the
474
+ # arguments specified.
475
+ #
476
+ # The @handlers must be a hash where key = event to handle and value is
477
+ # a Proc object (via Class.method(:name) or just proc {...}).
478
+ # This should be fine if you're setting up handlers with the prepend_handler
479
+ # method, but if you get "clever," you're on your own.
480
+ def handle(event, *arguments)
481
+ # Don't bother with anything if there are no handlers registered.
482
+ return unless Array === @handlers[event]
483
+
484
+ report "+++EVENT HANDLER: Handling event #{event} via #{@handlers[event].inspect}:" if @loud
485
+
486
+ # To allow others to modify args without messing up potentially important
487
+ # internal data, dupe the array using a deep copy. Hacky for sure, but
488
+ # effective.
489
+ new_args = Marshal.load(Marshal.dump(arguments))
490
+
491
+ # Call all hooks in order until one breaks the chain. For incoming
492
+ # events, we want something to break the chain or else it'll likely
493
+ # hit a reporter. For outgoing events, we tend to report them anyway,
494
+ # so no need to worry about ending the chain except when the bot wants
495
+ # to take full control over them.
496
+ result = false
497
+ for handler in @handlers[event]
498
+ result = handler.call(*new_args)
499
+ break if result == true
500
+ end
501
+ end
502
+
503
+ # Since numerics are so many and so varied, this method will auto-fallback
504
+ # to a simple report if no handler was defined.
505
+ def handle_numeric(number, fullactor, actor, target, text)
506
+ # All numerics share the same args, and rarely care about anything but
507
+ # text, so let's make it easier by passing a hash instead of a list
508
+ args = {:fullactor => fullactor, :actor => actor, :target => target}
509
+ base_event = :"incoming_numeric_#{number}"
510
+ if Array === @handlers[base_event]
511
+ handle(base_event, text, args)
512
+ else
513
+ # No handler = report and don't worry about it
514
+ report "Unknown raw #{number.to_s} from #{fullactor}: #{text}"
515
+ end
516
+ end
517
+
518
+ # Reports may not get printed in the proper order since I scrubbed the
519
+ # IRCSocket report capturing, but this is way more straightforward to me.
520
+ def report(*lines)
521
+ lines.each {|line| $stdout.puts "(#{Time.now.strftime('%H:%M.%S')}) #{line}"}
522
+ end
523
+ end
524
+
525
+ end