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.
- data/IRCBot.rb +132 -0
- data/README +66 -0
- data/lib/net/yail.rb +525 -0
- data/lib/net/yail/default_events.rb +192 -0
- data/lib/net/yail/eventmap.yml +248 -0
- data/lib/net/yail/magic_events.rb +55 -0
- data/lib/net/yail/output_api.rb +134 -0
- metadata +54 -0
data/IRCBot.rb
ADDED
@@ -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.
|
data/lib/net/yail.rb
ADDED
@@ -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
|