net-yail 1.4.6 → 1.5.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -42,29 +42,28 @@ class LoggerBot < IRCBot
42
42
  # Add hooks on startup (base class's start method calls add_custom_handlers)
43
43
  def add_custom_handlers
44
44
  # Set up hooks
45
- @irc.prepend_handler(:incoming_msg, self.method(:_in_msg))
46
- @irc.prepend_handler(:incoming_act, self.method(:_in_act))
47
- @irc.prepend_handler(:incoming_invite, self.method(:_in_invited))
48
- @irc.prepend_handler(:incoming_kick, self.method(:_in_kick))
49
-
50
- @irc.prepend_handler(:outgoing_join, self.method(:_out_join))
45
+ @irc.on_msg self.method(:_in_msg)
46
+ @irc.on_act self.method(:_in_act)
47
+ @irc.on_invite self.method(:_in_invited)
48
+ @irc.on_kick self.method(:_in_kick)
49
+ @irc.saying_join self.method(:_out_join)
51
50
  end
52
51
 
53
52
  private
54
53
  # Incoming message handler
55
- def _in_msg(fullactor, user, channel, text)
54
+ def _in_msg(event)
56
55
  # check if this is a /msg command, or normal channel talk
57
- if channel =~ /#{bot_name}/
58
- incoming_private_message(user, text)
56
+ if event.pm?
57
+ incoming_private_message(event.nick, event.message)
59
58
  else
60
- incoming_channel_message(user, channel, text)
59
+ incoming_channel_message(event.nick, event.channel, event.message)
61
60
  end
62
61
  end
63
62
 
64
- def _in_act(fullactor, user, channel, text)
63
+ def _in_act(event)
65
64
  # check if this is a /msg command, or normal channel talk
66
- return if (channel =~ /#{bot_name}/)
67
- log_channel_message(user, channel, "#{user} #{text}")
65
+ return if event.pm?
66
+ log_channel_message(event.nick, event.channel, "#{event.nick} #{event.message}")
68
67
  end
69
68
 
70
69
  # TODO: recalls the most recent logs for a given channel by reading from
@@ -136,23 +135,23 @@ class LoggerBot < IRCBot
136
135
 
137
136
  # Invited to a channel for logging purposes - simply auto-join for now.
138
137
  # Maybe allow only @master one day, or array of authorized users.
139
- def _in_invited(fullactor, actor, target)
140
- join target
138
+ def _in_invited(event)
139
+ join event.channel
141
140
  end
142
141
 
143
142
  # If bot is kicked, he must rejoin!
144
- def _in_kick(fullactor, actor, target, object, text)
145
- if object == bot_name
143
+ def _in_kick(event)
144
+ if event.target == bot_name
146
145
  # Rejoin almost immediately - logging is important.
147
- join target
146
+ join event.channel
148
147
  end
149
148
 
150
149
  return true
151
150
  end
152
151
 
153
152
  # We're trying to join a channel - use key if we have one
154
- def _out_join(target, pass)
155
- key = @passwords[target]
156
- pass.replace(key) unless key.to_s.empty?
153
+ def _out_join(event)
154
+ key = @passwords[event.channel]
155
+ event.password.replace(key) unless key.to_s.empty?
157
156
  end
158
157
  end
@@ -0,0 +1,114 @@
1
+ # This is a demonstration of net-yail just to see what a "real" bot could do without much work.
2
+ # Chances are good that you'll want to put things in classes and/or modules rather than go this
3
+ # route, so take this example with a grain of salt.
4
+ #
5
+ # Yes, this is a very simple copy of an existing "loudbot" implementation, but using YAIL to
6
+ # demonstrate the INCREDIBLE POWER THAT IS Net::YAIL. Plus, plagiarism is a subset of the cool
7
+ # crime of stealing.
8
+ #
9
+ # Example of running this thing:
10
+ # ruby bot_runner.rb --network irc.somewhere.org --channel "#bots"
11
+
12
+ require 'rubygems'
13
+
14
+ # Want a specific version of net/yail? Try uncommenting this:
15
+ # gem 'net-yail', '1.x.y'
16
+
17
+ require 'net/yail'
18
+ require 'getopt/long'
19
+
20
+ # Hacks Array#shuffle and Array#shuffle! for people not using the latest ruby
21
+ require 'shuffle'
22
+
23
+ # Pulls in all of loudbot's methods - filter/callback handlers for IRC events
24
+ require 'loudbot'
25
+
26
+ # User specifies network, channel and nick
27
+ opt = Getopt::Long.getopts(
28
+ ['--network', Getopt::REQUIRED],
29
+ ['--channel', Getopt::REQUIRED],
30
+ ['--nick', Getopt::REQUIRED],
31
+ ['--debug', Getopt::BOOLEAN]
32
+ )
33
+
34
+ # Create bot object
35
+ @irc = Net::YAIL.new(
36
+ :address => opt['network'],
37
+ :username => 'Frakking Bot',
38
+ :realname => 'John Botfrakker',
39
+ :nicknames => [opt['nick'] || "SUPERLOUD"]
40
+ )
41
+
42
+ # Loud messages can be newline-separated strings in louds.txt or an array or hash serialized in
43
+ # louds.yml. If messages are an array, we convert all of them to hash keys with a score of 1.
44
+ @messages = FileTest.exist?("louds.yml") ? YAML.load_file("louds.yml") :
45
+ FileTest.exist?("louds.txt") ? IO.readlines("louds.txt") :
46
+ {"ROCK ON WITH SUPERLOUD" => 1}
47
+ if Array === @messages
48
+ dupes = @messages.dup
49
+ @messages = {}
50
+ dupes.each {|string| @messages[string.strip] = 1}
51
+ end
52
+
53
+ @random_messages = @messages.keys.shuffle
54
+ @last_message = nil
55
+ @dirty_messages = false
56
+
57
+ # If --debug is passed on the command line, we spew lots of filth at the user
58
+ @irc.log.level = Logger::DEBUG if opt['debug']
59
+
60
+ #####
61
+ #
62
+ # To learn the YAIL, begin below with attentiveness to commented wording
63
+ #
64
+ #####
65
+
66
+ # This is a filter. Because it's past-tense ("heard"), it runs after the server's welcome message
67
+ # has been read - i.e., after any before-filters and the main hanler happen.
68
+ @irc.heard_welcome { |e| @irc.join(opt['channel']) if opt['channel'] }
69
+
70
+ # on_xxx means it's a callback for an incoming event. Callbacks run after before-filters, and
71
+ # replaces any existing incoming invite callback. YAIL has very few built-in callbacks, so
72
+ # this is a safe operation.
73
+ @irc.on_invite { |e| @irc.join(e.channel) }
74
+
75
+ # This is just another callback, using the do/end block form. We auto-message the channel on join.
76
+ @irc.on_join do |e|
77
+ @irc.msg(e.channel, "WHATS WRONG WITH BEING SEXY") if e.nick == @irc.me
78
+ end
79
+
80
+ # You should *never* override the on_ping callback unless you handle the PONG manually!!
81
+ # Filters, however, are perfectly fine.
82
+ #
83
+ # Here we're using the ping filter to actually do the serialization of our messages hash. Since
84
+ # we know pings are regular, this is kind of a hack to serialize every few minutes.
85
+ @irc.heard_ping do
86
+ unless @dirty_messages
87
+ File.open("louds.yml", "w") {|f| f.puts @messages.to_yaml}
88
+ @dirty_messages = false
89
+ end
90
+ end
91
+
92
+ # This is a before-filter - using the present tense means it's a before-filter, and using a tense
93
+ # of "hear" means it's for incoming messages (as opposed to "saying" and "said", where we'd filter
94
+ # our outgoing messages). Here we intercept all potential commands and send them to a method.
95
+ @irc.hearing_msg {|e| do_command($1, e) if e.message =~ /^!(.*)$/ }
96
+
97
+ # Another filter, but in-line this time - we intercept messages directly to the bot. The call to
98
+ # +handled!+ tells the event not to run any more filters or the main callback.
99
+ @irc.hearing_msg do |e|
100
+ if e.message =~ /^#{@irc.me}/
101
+ random_message(e.channel)
102
+ e.handled!
103
+ end
104
+ end
105
+
106
+ # This is our primary message callback. We know our filters have caught people talking to us and
107
+ # any command-style messages, so we don't need to worry about those situations here. The decision
108
+ # to make this the primary callback is pretty arbitrary - do what makes the most sense to you.
109
+ #
110
+ # Note that this is a proc-based filter - we handle the message entirely in incoming_message.
111
+ @irc.on_msg self.method(:incoming_message)
112
+
113
+ # Start the bot - the bang (!) calls the version of start_listening that runs an endless loop
114
+ @irc.start_listening!
@@ -0,0 +1,87 @@
1
+ # Stores a LOUD message into the hash and responds.
2
+ def it_was_loud(message, channel)
3
+ @irc.log.debug "IT WAS LOUD! #{message.inspect}"
4
+
5
+ @messages[message] ||= 1
6
+ random_message(channel)
7
+ end
8
+
9
+ # This is our main message handler.
10
+ #
11
+ # We store and respond if messages meet the following criteria:
12
+ # * It is long (11 characters or more)
13
+ # * It has at least one space
14
+ # * It has no lowercase letters
15
+ # * At least 60% of the characters are uppercase letters
16
+ def incoming_message(e)
17
+ # We don't respond to "private" messages
18
+ return if e.pm?
19
+
20
+ text = e.message
21
+
22
+ return if text =~ /[a-z]/
23
+
24
+ # Count various exciting things
25
+ len = text.length
26
+ uppercase_count = text.scan(/[A-Z]/).length
27
+ space_count = text.scan(/\s/).length
28
+
29
+ if len >= 11 && uppercase_count >= (len * 0.60) && space_count >= 1
30
+ it_was_loud(e.message, e.channel)
31
+ end
32
+ end
33
+
34
+ # Pulls a random message from our messages array and sends it to the given channel. Reshuffles
35
+ # the main array if the randomized array is empty.
36
+ def random_message(channel)
37
+ @random_messages = @messages.keys.shuffle if @random_messages.empty?
38
+ @last_message = @random_messages.pop
39
+ @irc.msg(channel, @last_message)
40
+ end
41
+
42
+ # Just keepin' the plagiarism alive, man. At least in my version, size is always based on requester.
43
+ def send_dong(channel, user_hash)
44
+ old_seed = srand(user_hash)
45
+ @irc.msg(channel, "8" + ('=' * (rand(20).to_i + 8)) + "D")
46
+ srand(old_seed)
47
+ end
48
+
49
+ # Adds +value+ to the score of the last message, if there was one. If the score goes too low, we
50
+ # remove that message forever.
51
+ def vote(value)
52
+ return unless @last_message
53
+
54
+ @messages[@last_message] += value
55
+ if @messages[@last_message] <= -1
56
+ @last_message = nil
57
+ @messages.delete(@last_message)
58
+ end
59
+ @dirty_messages = true
60
+ end
61
+
62
+ # Reports the last message's score
63
+ def score(channel)
64
+ if !@last_message
65
+ @irc.msg(channel, "NO LAST MESSAGE OR IT WAS DELETED BY !DOWNVOTE")
66
+ return
67
+ end
68
+
69
+ @irc.msg(channel, "#{@last_message}: #{@messages[@last_message]}")
70
+ end
71
+
72
+ # Handles a command (string begins with ! - to keep with the pattern, I'm making our loudbot only
73
+ # respond to loud commands)
74
+ def do_command(command, e)
75
+ case command
76
+ when "DONGME" then send_dong(e.channel, e.msg.user.hash + e.msg.host.hash)
77
+ when "UPVOTE" then vote(1)
78
+ when "DOWNVOTE" then vote(-1)
79
+ when "SCORE" then score(e.channel)
80
+ when "HELP" then @irc.msg(e.channel, "I HAVE COMMANDS AND THEY ARE !DONGME !UPVOTE !DOWNVOTE !SCORE AND !HELP")
81
+ end
82
+
83
+ # Here we're saying that we don't want any other handling run - no filters, no handler. For
84
+ # commands, I put this here because I know I don't want any other handlers having to deal with
85
+ # strings beginning with a bang.
86
+ e.handled!
87
+ end
@@ -0,0 +1,17 @@
1
+ # Dynamically adds Array shuffling as described in http://www.ruby-forum.com/topic/163649
2
+ class Array
3
+ # Shuffle the array
4
+ def shuffle!
5
+ n = length
6
+ for i in 0...n
7
+ r = Kernel.rand(n-i)+i
8
+ self[r], self[i] = self[i], self[r]
9
+ end
10
+ self
11
+ end
12
+
13
+ # Return a shuffled copy of the array
14
+ def shuffle
15
+ dup.shuffle!
16
+ end
17
+ end
@@ -1,4 +1,8 @@
1
1
  require 'rubygems'
2
+
3
+ # Want a specific version of net/yail? Try uncommenting this:
4
+ # gem 'net-yail', '1.x.y'
5
+
2
6
  require 'net/yail'
3
7
  require 'getopt/long'
4
8
 
@@ -13,13 +17,14 @@ irc = Net::YAIL.new(
13
17
  :address => opt['network'],
14
18
  :username => 'Frakking Bot',
15
19
  :realname => 'John Botfrakker',
16
- :nicknames => [opt['nick']],
17
- :loud => opt['loud']
20
+ :nicknames => [opt['nick']]
18
21
  )
19
22
 
23
+ irc.log.level = Logger::DEBUG if opt['loud']
24
+
20
25
  # Register handlers
21
- irc.prepend_handler(:incoming_welcome) {|text, args| irc.join('#bots') }
22
- irc.prepend_handler(:incoming_invite) {|full, user, channel| irc.join(channel) }
26
+ irc.heard_welcome { |e| irc.join('#bots') } # Filter - runs after the server's welcome message is read
27
+ irc.on_invite { |e| irc.join(e.channel) } # Handler - runs on an invite message
23
28
 
24
29
  # Start the bot and enjoy the endless loop
25
30
  irc.start_listening!
@@ -9,6 +9,7 @@ require 'logger'
9
9
  require 'net/yail/magic_events'
10
10
  require 'net/yail/default_events'
11
11
  require 'net/yail/output_api'
12
+ require 'net/yail/legacy_events'
12
13
 
13
14
  # This tells us our version info.
14
15
  require 'net/yail/yail-version'
@@ -25,215 +26,234 @@ module Net
25
26
  # This library is based on the initial release of IRCSocket with a tiny bit
26
27
  # of plagarism of Ruby-IRC.
27
28
  #
29
+ # Need an example? For a separate project you can play with that relies on Net::YAIL, check out
30
+ # https://github.com/Nerdmaster/superloud. This is based on the code in the examples directory,
31
+ # but is easier to clone, run, and tinker with because it's a separate github project.
32
+ #
28
33
  # My aim here is to build something that is still fairly simple to use, but
29
34
  # powerful enough to build a decent IRC program.
30
35
  #
31
36
  # This is far from complete, but it does successfully power a relatively
32
37
  # complicated bot, so I believe it's solid and "good enough" for basic tasks.
33
38
  #
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_any(raw) - "global" handler that catches all events and may
47
- # modify their data as necessary before the real handler is hit. This should
48
- # only be used in cases where it's necessary to grab a lot of events that
49
- # also need to be processed elsewhere, such as doing input filtering for
50
- # all events that have a "text" element. Note that returning true from
51
- # this handler will still break the entire chain of event handling.
52
- # * :incoming_msg(fullactor, actor, target, text) - Normal message from actor to target
53
- # * :incoming_act(fullactor, actor, target, text) - CTCP "action" (emote) from actor to target
54
- # * :incoming_invite(fullactor, actor, target) - INVITE to target channel from actor
55
- # * :incoming_ctcp(fullactor, actor, target, text) - CTCP other than "action" from actor to target
56
- # * :incoming_ctcpreply(fullactor, actor, target, text) - CTCP NOTICE from actor to target
57
- # * :incoming_notice(fullactor, actor, target, text) - other NOTICE from actor to target
58
- # * :incoming_mode(fullactor, actor, target, modes, objects) - actor sets modes on objects in target channel
59
- # * :incoming_topic_change(fullactor, actor, channel, text) - actor sets channel topic to given text string
60
- # * :incoming_join(fullactor, actor, target) - actor joins target channel
61
- # * :incoming_part(fullactor, actor, target, text) - actor leaves target with message in text
62
- # * :incoming_kick(fullactor, actor, target, object, text) - actor kicked object from target with reason 'text'
63
- # * :incoming_quit(fullactor, actor, text) - actor left server completely with reason 'text'
64
- # * :incoming_nick(fullactor, actor, nickname) - actor changed to nickname
65
- # * :incoming_ping(text) - ping from server with given text
66
- # * :incoming_miscellany(line) - text from server didn't match anything known
67
- # * :incoming_welcome(text, args) - raw 001 from server, means we successfully logged in
68
- # * :incoming_bannedfromchan(text, args) - banned from channel
69
- # * Anything else in the eventmap.yml file with params(text, args).
70
- #
71
- # Common parameter elements:
72
- # * fullactor: Rarely needed, full text of origin of an action
73
- # * actor: Nickname of originator of an action
74
- # * target: Nickname for private actions, channel name for public
75
- # * text: Actual message/emote/notice/etc
76
- # * args: For numeric handlers, this is a hash of :fullactor, :actor, and
77
- # :target. Most numeric handlers I've built don't need this, so I made it
78
- # easier to just get what you specifically want.
79
- #
80
- # ==Outgoing Events
81
- #
82
- # Generally speaking, you won't need these very often, but they're here for
83
- # the edge cases all the same. Note that the socket output cannot be skipped
84
- # (see "Return value from events" below), so this is truly just to allow
85
- # modifying things before they go out (filtering speech, converting or
86
- # stripping markup, etc) or just general stats-type logic.
87
- #
88
- # Note that in all cases below, the client is *about* to perform an action.
89
- # Text can be filtered, things can be logged, but keep in mind that the action
90
- # has not yet happened.
91
- #
92
- # Events:
93
- # * :outgoing_begin_connection(username, address, realname) - called when the
94
- # start_listening method has set up all threading and such. Default behavior
95
- # is to call user() and nick()
96
- # * :outgoing_privmsg(target, text) - Any kind of PRIVMSG output is about to
97
- # get sent out
98
- # * :outgoing_msg(target, text) - Hit by a direct call to msg, which is
99
- # normally used for "plain" messages, but a "clever" user could do their own
100
- # CTCP messages here as well. Shoot them if they do.
101
- # * :outgoing_ctcp(target, text) - All CTCP messages hit here eventually
102
- # * :outgoing_act(target, text) - ACTION CTCP messages should go through this,
103
- # not manually use ctcp.
104
- # * :outgoing_notice(target, text) - All NOTICE messages hit here
105
- # * :outgoing_ctcpreply(target, text) - CTCP NOTICE messages
106
- # * :outgoing_mode(target, modes, objects) - Sets or queries mode. If modes is
107
- # present, sends mode list to target. Objects would be users.
108
- # * :outgoing_join(target, pass) - About to attempt to join target channel with
109
- # given password (pass is '' if not specified in the join() command)
110
- # * :outgoing_part(target, text) - The given target channel is about to be
111
- # left, with optional text reason.
112
- # * :outgoing_quit(text) - The client is about to quit, with optional text
113
- # reason.
114
- # * :outgoing_nick(new_nick) - The client is about to change nickname
115
- # * :outgoing_user(username, myaddress, address, realname) - We're about to
116
- # send a USER command.
117
- # * :outgoing_pass(password) - The client is about to send a password to the
118
- # server via PASS.
119
- # * :outgoing_oper(user, password) - The client is about to request ops from
120
- # the server via OPER.
121
- # * :outgoing_topic(channel, new_topic) - If new_topic is blank (nil or ''),
122
- # the client is requesting the channel's topic, otherwise setting it.
123
- # * :outgoing_names(channel) - Client is requesting a list of names in the
124
- # given channel, or all channels and names if channel is blank.
125
- # * :outgoing_list(channel, server) - Client is querying channel information.
126
- # I honestly don't know what server is for from RFC, but asking for a
127
- # specific channel gives just data on that channel.
128
- # * :outgoing_invite(nick, channel) - Client is sending an INVITE message to
129
- # nick for channel.
130
- # * :outgoing_kick(nick, channel, comment) - Client is about to kick the user
131
- # from the channel with an optional comment.
132
- #
133
- # Note that a single output call can hit multiple handlers, so you must plan
134
- # carefully. A call to act() will hit the act handler, then ctcp (since act
135
- # is a type of ctcp message), then privmsg.
136
- #
137
- # ==Custom Events
138
- #
139
- # Yes, you can register your own wacky event handlers if you like, and have
140
- # your code call them. Just register a handler with some funky name of
141
- # your own design (avoid the prefixes :incoming and :outgoing for obvious
142
- # reasons), and so long as something calls that handler, your handler method
143
- # will get its data.
144
- #
145
- # This isn't likely useful for a simple program, but for a subclass or wrapper
146
- # of the IRC class, having the ability to give *its* users new events without
147
- # mucking up this class can be helpful. For instance, see IRCBot#irc_loop
148
- # and the :irc_loop event. If one wants their bot to do something regularly,
149
- # they just handle that event and get frequent calls.
150
- #
151
- # ==Return value from events
152
- #
153
- # The return can be *critical* - a true value tells the handlers to stop
154
- # their chain (true = "yes, I handled this event, stay the frak away you
155
- # other, lesser handlers!), so no other handlers will be called.
156
- #
157
- # Note that critical handlers (incoming ping, welcome, and nick change) cannot
158
- # be overwritten as they actually run *before* user-defined handlers, and
159
- # output handlers are just for filtering and cannot stop the socket from
160
- # sending its data. If you want to change that low-level stuff, you should
161
- # subclass, modify the code directly, monkey-patch, or just write your own
162
- # library.
163
- #
164
- # When should you return false from an event? Generally any time you have a
165
- # handler that really needs to report itself. Unless you have multiple
166
- # layers of handlers for a given event, there's little reason to worry about
167
- # breaking the chain of events. Since handlers are *prepended* to the list,
168
- # anybody subclassing your code can override your events, not the other way
169
- # around. The main use is if you have multiple handlers for a single complex
170
- # event, where you want each handler to do its own set process and pass on the
171
- # event if it isn't resposible for that particular situation. Allows complex
172
- # interactions to be made a bit cleaner, theoretically.
173
- #
174
- # =Simple example
175
- #
176
- # For a program to do anything useful, it must instantiate an object with
177
- # useful data and register some handlers:
178
- #
179
- # require 'rubygems'
180
- # require 'net/yail'
181
- #
182
- # irc = Net::YAIL.new(
183
- # :address => 'irc.someplace.co.uk',
184
- # :username => 'Frakking Bot',
185
- # :realname => 'John Botfrakker',
186
- # :nicknames => ['bot1', 'bot2', 'bot3']
187
- # )
188
- #
189
- # irc.prepend_handler :incoming_welcome, proc {|text, args|
190
- # irc.join('#foo')
191
- # return false
192
- # }
193
- #
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
- # Now we've built a simple IRC listener that will connect to a (probably
201
- # invalid) network, identify itself, and sit around waiting for the welcome
202
- # message. After this has occurred, we join a channel and return false.
203
- #
204
- # One could also define a method instead of a proc:
205
- #
206
- # require 'rubygems'
207
- # require 'net/yail'
208
- #
209
- # def welcome(text, args)
210
- # @irc.join('#channel')
211
- # return false
212
- # end
213
- #
214
- # irc = Net::YAIL.new(
215
- # :address => 'irc.someplace.co.uk',
216
- # :username => 'Frakking Bot',
217
- # :realname => 'John Botfrakker',
218
- # :nicknames => ['bot1', 'bot2', 'bot3']
219
- # )
220
- #
221
- # irc.prepend_handler :incoming_welcome, method(:welcome)
222
- # irc.start_listening
223
- # while irc.dead_socket == false
224
- # # Avoid major CPU overuse by taking a very short nap
225
- # sleep 0.05
226
- # end
227
- #
228
- # =Better example
229
- #
230
- # See the included logger bot (under the examples directory of this project)
231
- # for use of the IRCBot base class. It's a fully working bot example with
232
- # real-world use.
39
+ # =Events overview
40
+ #
41
+ # YAIL at its core is an event handler with some logic specific to IRC socket messages. BaseEvent
42
+ # is the parent of all event objects. An event is run through various pre-callback filters, a
43
+ # single callback, and post-callback filters. Up until the post-callback filters start, the handler
44
+ # "chain" can be stopped by calling the event's .handled! method. It is generally advised against
45
+ # doing this, as it will stop things like post-callback stats gathering and similar plugin-friendly
46
+ # features, but it does make sense in certain situations (an "ignore user" module, for instance).
47
+ #
48
+ # The life of a typical event, such as the one generated when a server message is parsed into a Net::YAIL::IncomingEvent object:
49
+ #
50
+ # * If the event hasn't been handled, the event's callback is run
51
+ # * If the event hasn't been handled, legacy handlers are run if any are registered (TO BE REMOVED IN 2.0)
52
+ # * Legacy handlers can return true to end the chain, much like calling <tt>BaseEvent#handle!</tt> on an event object
53
+ # * If the event hasn't been handled, all "after filters" are run (these cannot set an event as having been handled)
54
+ #
55
+ # ==Callbacks and Filters
56
+ #
57
+ # Callbacks and filters are basically handlers for a given event. The difference in a callback
58
+ # and filter is explained above (1 callback per event, many filters), but at their core they are
59
+ # just code that handles some aspect of the event.
60
+ #
61
+ # All handlers require some block of code. You can explicitly create a Proc object, use
62
+ # Something.method, or just pass in a block. The block yields the event object which will have
63
+ # all relevant data for the event. See the examples below for a basic idea.
64
+ #
65
+ # To register an event's callback, you have the following options:
66
+ # * <tt>set_callback(event_type, method = nil, &block)</tt>: Sets the event type's callback, clobbering any
67
+ # existing callback for that event type.
68
+ # * <tt>on_xxx(method = nil, &block)</tt>: For incoming events only, this is a shortcut for <tt>set_callback</tt>.
69
+ # The "xxx" must be replaced by the incoming event's short type name. For example,
70
+ # <tt>on_welcome {|event| ...}</tt> would be used in place of <tt>set_callback(:incoming_welcome, xxx)</tt>.
71
+ #
72
+ # To register a before- or after-callback filter:
73
+ # * <tt>before_filter(event_type, method = nil, &block)</tt>: Sets a before-callback filter, adding it to
74
+ # the current list of before-callback filters for the given event type.
75
+ # * <tt>after_filter(event_type, method = nil, &block)</tt>: Sets an after-callback filter, adding it to
76
+ # the current list of after-callback filters for the given event type.
77
+ # * <tt>hearing_xxx(method = nil, &block)</tt>: Adds a before-callback filter for the given incoming event
78
+ # type, such as <tt>hearing_msg {|event| ...}</tt>
79
+ # * <tt>heard_xxx(method = nil, &block)</tt>: Adds an after-callback filter for the given incoming event
80
+ # type, such as <tt>heard_msg {|event| ...}</tt>
81
+ # * <tt>saying_xxx(method = nil, &block)</tt>: Adds a before-callback filter for the given outgoing event
82
+ # type, such as <tt>saying_mode {|event| ...}</tt>
83
+ # * <tt>said_xxx(method = nil, &block)</tt>: Adds an after-callback filter for the given outgoing event
84
+ # type, such as <tt>said_act {|event| ...}</tt>
85
+ #
86
+ # ==Incoming events
87
+ #
88
+ # *All* incoming events will have, at the least, the following methods:
89
+ # * <tt>raw</tt>: The raw text sent by the IRC server
90
+ # * <tt>msg</tt>: The parsed IRC message (Net::YAIL::MessageParser instance)
91
+ # * <tt>server?</tt>: Boolean flag. True if the message was generated by the server alone, false if it
92
+ # was generated by some kind of user action (such as a PRIVMSG sent from somebody else)
93
+ # * <tt>from</tt>: Originator of message: user's nickname if a user message, server name otherwise
94
+ #
95
+ # Additionally, *all messages originated by another IRC user* will have these methods:
96
+ # * <tt>fullname</tt>: The full username ("Nerdmaster!jeremy@nerdbucket.com", for instance)
97
+ # * <tt>nick</tt>: The short nickname of a user ("Nerdmaster", for instance) - this will be the
98
+ # same as <tt>event.from</tt>, but obviously only for user-initiated events.
99
+ #
100
+ # Messages sent by the server that weren't initiated by a user will have <tt>event.servername</tt>,
101
+ # which is merely the name of the server, and will be the same as <tt>event.from</tt>.
102
+ #
103
+ # When in doubt, you can always build a filter for a particular event that spits out all its
104
+ # non-base methods:
105
+ # yail.hearing_xxx {|e| puts e.public_methods - Net::YAIL::BaseEvent.instance_methods}
106
+ #
107
+ # This should be a comprehensive list of all incoming events and what additional attributes the
108
+ # object will expose.
109
+ #
110
+ # * <tt>:incoming_any</tt>: A catch-all handler useful for reporting or doing top-level filtering.
111
+ # Before- and after-callback filters can run for all events by adding them to :incoming_any, but
112
+ # you cannot register a callback, as the event's type determines its callback. :incoming_any
113
+ # before-callback filters can stop an event from happening on a global scale, so be careful when
114
+ # deciding to do anything "clever" here.
115
+ # * <tt>:incoming_error</tt>: A server error of some kind happened. <tt>event.message</tt> gives you the message sent
116
+ # by the server.
117
+ # * <tt>:incoming_ping</tt>: PING from server. YAIL handles this by default, so if you override the
118
+ # handler, you MUST send a PONG response or the server will close your connection. <tt>event.message</tt>
119
+ # may have a PING "message" in it. The return PONG should send out the same message as the PING
120
+ # received.
121
+ # * <tt>:incoming_topic_change</tt>: The topic of a channel was changed. <tt>event.channel</tt> gives you the
122
+ # channel in which the change occurred, while <tt>event.message</tt> gives you the message, i.e. the new topic.
123
+ # * <tt>:incoming_numeric_###</tt>: If you want, you can set up your handlers for numeric events by number,
124
+ # but you'll have a much easier time looking at the eventmap.yml file included in the lib/net/yail
125
+ # directory. You can create an incoming handler for any event in that file. The event names will
126
+ # be <tt>:incoming_xxx</tt>, where "xxx" is the text of the event. For instance, you could use
127
+ # <tt>set_callback(:incoming_liststart) {|event| ...}</tt> to handle the 321 numeric message, or just
128
+ # <tt>on_liststart {|event| ...}</tt>. Exposes <tt>event.target</tt>, <tt>event.parameters</tt>,
129
+ # <tt>event.message</tt>, and <tt>event.numeric</tt>. You may have to experiment with different
130
+ # numerics to see what this data actually means for a given event.
131
+ # * <tt>:incoming_invite</tt>: INVITE message sent from a user to request your presence in another channel.
132
+ # Exposes <tt>event.channel</tt>, the channel in question, and <tt>event.target</tt>, which should always be
133
+ # your nickname.
134
+ # * <tt>:incoming_join</tt>: A user joined a channel. <tt>event.channel</tt> tells you the channel.
135
+ # * <tt>:incoming_part</tt>: A user left a channel. <tt>event.channel</tt> tells you the channel, and
136
+ # <tt>event.message</tt> will contain a message if the user gave one.
137
+ # * <tt>:incoming_kick</tt>: A user was kicked from a channel. <tt>event.channel</tt> tells you
138
+ # the channel, <tt>event.target</tt> tells you the nickname of the kicked party, and
139
+ # <tt>event.message</tt> will contain a message if the kicking party gave one.
140
+ # * <tt>:incoming_quit</tt>: A user quit the server. <tt>event.message</tt> will have details, if the
141
+ # user provided a quit message.
142
+ # * <tt>:incoming_nick</tt>: A user changed nicknames. <tt>event.message</tt> will contain the new
143
+ # nickname.
144
+ # * <tt>:incoming_mode</tt>: A user or server can initiate this, and this is the most screwy event
145
+ # in YAIL. This needs an overhaul and will hopefully change by 2.0, but for now I take the raw
146
+ # mode strings, such as "+bivv" and put them in <tt>event.message</tt>. All arguments of the
147
+ # mode strings get stored as individual records in the <tt>event.targets</tt> array. For modes
148
+ # like "+ob", the first entry in targets will be the user given ops, and the second will be the
149
+ # ban string. I hope to overhaul this prior to 2.0, so if you rely on mode parsing, be warned.
150
+ # * <tt>:incoming_msg</tt>: A "standard" PRIVMSG event (i.e., not CTCP). <tt>event.message</tt> will
151
+ # contain the message, obviously. If the message is to a channel, <tt>event.channel</tt>
152
+ # will contain the channel name, <tt>event.target</tt> will be nil, and <tt>event.pm?</tt> will
153
+ # be false. If the message is sent to a user (the client running Net::YAIL),
154
+ # <tt>event.channel</tt> will be nil, <tt>event.target</tt> will have the user name, and
155
+ # <tt>event.pm?</tt> will be true.
156
+ # * <tt>:incoming_ctcp</tt>: The behavior of <tt>event.target</tt>, <tt>event.channel</tt>, and
157
+ # <tt>event.pm?</tt> will remain the same as for <tt>:incoming_msg</tt> events.
158
+ # <tt>event.message</tt> will contain the CTCP message.
159
+ # * <tt>:incoming_act</tt>: The behavior of <tt>event.target</tt>, <tt>event.channel</tt>, and
160
+ # <tt>event.pm?</tt> will remain the same as for <tt>:incoming_msg</tt> events.
161
+ # <tt>event.message</tt> will contain the ACTION message.
162
+ # * <tt>:incoming_notice</tt>: The behavior of <tt>event.target</tt>, <tt>event.channel</tt>, and
163
+ # <tt>event.pm?</tt> will remain the same as for <tt>:incoming_msg</tt> events.
164
+ # <tt>event.message</tt> will contain the NOTICE message.
165
+ # * <tt>:incoming_ctcp_reply</tt>: The behavior of <tt>event.target</tt>, <tt>event.channel</tt>,
166
+ # and <tt>event.pm?</tt> will remain the same as for <tt>:incoming_msg</tt> events.
167
+ # <tt>event.message</tt> will contain the CTCP reply message.
168
+ # * <tt>:incoming_unknown</tt>: This should NEVER happen, but just in case, it's there. Enjoy!
169
+ #
170
+ # ==Output API
171
+ #
172
+ # All output API calls create a Net::YAIL::OutgoingEvent object and dispatch that event. After
173
+ # before-callback filters are processed, assuming the event wasn't handled, the callback will send
174
+ # the message out to the IRC socket. If you choose to override the callback for outgoing events,
175
+ # rather than using filters, you will have to print the data to the socket yourself.
176
+ #
177
+ # The parameters for the API calls will match what the outgoing event object exposes as attributes,
178
+ # so if there were an API call for "foo(bar, baz)", it would generate an outgoing event of type
179
+ # :outgoing_foo. The data you passed in as "bar" would be available via <tt>event.bar</tt> in a handler.
180
+ #
181
+ # There is also an :outgoing_any event type that can be used for global filtering much like the
182
+ # :incoming_any filtering.
183
+ #
184
+ # The <tt>:outgoing_begin_connection</tt> event callback should never be overwritten. It exists so
185
+ # you can add filters before or after the initial flurry of messages to the server (USER, PASS, and
186
+ # NICK), but it is really an internal "helper" event. Overwriting it means you will need to write
187
+ # your own code to log in to the server.
188
+ #
189
+ # This should be a comprehensive list of all outgoing methods and parameters:
190
+ #
191
+ # * <tt>msg(target, message)</tt>: Send a PRIVMSG to the given target (channel or nickname)
192
+ # * <tt>ctcp(target, message)</tt>: Sends a PRIVMSG to the given target with its message wrapped in
193
+ # ASCII character 1, signifying use of client-to-client protocol.
194
+ # * <tt>act(target, message)</tt>: Sends a PRIVMSG to the given target with its message wrapped in the
195
+ # CTCP "action" syntax. A lot of IRC clients use "/me" to do this command.
196
+ # * <tt>privmsg(target, message)</tt>: Sends a raw, unbuffered PRIVMSG to the given target - primarily
197
+ # useful for filtering, as msg, act, and ctcp all eventually call this handler.
198
+ # * <tt>notice(target, message)</tt>: Sends a notice message to the given target
199
+ # * <tt>ctcpreply(target, message)</tt>: Sends a notice message wrapped in ASCII 1 to signify a CTCP reply.
200
+ # * <tt>mode(target, [modes, [objects]])</tt>: Sets or requests modes for the given target
201
+ # (channel or user). The list of modes, if present, is applied to the target and objects if
202
+ # present. Modes in YAIL need some work, but here are some basic examples:
203
+ # * <tt>mode("#channel", "+b", "Nerdmaster!*@*")</tt>: bans anybody with the nickname
204
+ # "Nerdmaster" from subsequently joining #channel.
205
+ # * <tt>mode("#channel")</tt>: Requests a list of modes on #channel
206
+ # * <tt>mode("#channel", "-k")</tt>: Removes the key for #channel
207
+ # * <tt>join(channel, [password])</tt>: Joins the given channel with an optional password (channel key)
208
+ # * <tt>part(channel, [message])</tt>: Leaves the given channel, with an optional message specified on part
209
+ # * <tt>quit([message])</tt>: Leaves the server with an optional message. Note that some servers will
210
+ # not display your quit message due to spam issues.
211
+ # * <tt>nick(nick)</tt>: Changes your nickname, and updates YAIL @me variable if successful
212
+ # * <tt>user(username, hostname, servername, realname)</tt>: Sets up your information upon joining
213
+ # a server. YAIL should generally take care of this for you in the default :outgoing_begin_connection
214
+ # callback.
215
+ # * <tt>pass(password)</tt>: Sends a server password, not to be confused with a channel key.
216
+ # * <tt>oper(user, password)</tt>: Authenticates a user as an IRC operator for the server.
217
+ # * <tt>topic(channel, [new_topic])</tt>: With no new_topic, returns the topic for a given channel.
218
+ # If new_topic is present, sets the topic instead.
219
+ # * <tt>names([channel])</tt>: Gets a list of all users on the network or a specific channel if specified.
220
+ # The channel parameter can actually contain a comma-separated list of channels if desired.
221
+ # * <tt>list([channel, [server]]</tt>: Shows all channels on the server. <tt>channel</tt> can
222
+ # contain a comma-separated list of channels, which will restrict the list to the given channels.
223
+ # If <tt>server</tt> is present, the request is forwarded to the given server.
224
+ # * <tt>invite(nick, channel)</tt>: Invites a user to the given channel.
225
+ # * <tt>kick(nick, channel, [message])</tt>: "KICK :channel :nick", :nick, :channel, :reason, " ::reason"
226
+ #
227
+ # =Simple Example
228
+ #
229
+ # You should grab the source from github (https://github.com/Nerdmaster/ruby-irc-yail) and look at
230
+ # the examples directory for more interesting (but still simple) examples. But to get you started,
231
+ # here's a really dumb, contrived example:
232
+ #
233
+ # require 'rubygems'
234
+ # require 'net/yail'
235
+ #
236
+ # irc = Net::YAIL.new(
237
+ # :address => 'irc.someplace.co.uk',
238
+ # :username => 'Frakking Bot',
239
+ # :realname => 'John Botfrakker',
240
+ # :nicknames => ['bot1', 'bot2', 'bot3']
241
+ # )
242
+ #
243
+ # # Automatically join #foo when the server welcomes us
244
+ # irc.on_welcome {|event| irc.join("#foo") }
245
+ #
246
+ # # Store the last message and person who spoke - this is a filter as it doesn't need to be
247
+ # # "the" definitive code run for the event
248
+ # irc.hearing_msg {|event| @last_message = {:nick => event.nick, :message => event.message} }
249
+ #
250
+ # # Loops forever until CTRL+C
251
+ # irc.start_listening!
233
252
  class YAIL
234
253
  include Net::IRCEvents::Magic
235
254
  include Net::IRCEvents::Defaults
236
255
  include Net::IRCOutputAPI
256
+ include Net::IRCEvents::LegacyEvents
237
257
 
238
258
  attr_reader(
239
259
  :me, # Nickname on the IRC server
@@ -248,20 +268,20 @@ class YAIL
248
268
  )
249
269
 
250
270
  def silent
251
- @log.warn '[DEPRECATED] - Net::YAIL#silent is deprecated as of 1.4.1'
271
+ @log.warn '[DEPRECATED] - Net::YAIL#silent is deprecated as of 1.4.1 - .log can be used instead'
252
272
  return @log_silent
253
273
  end
254
274
  def silent=(val)
255
- @log.warn '[DEPRECATED] - Net::YAIL#silent= is deprecated as of 1.4.1'
275
+ @log.warn '[DEPRECATED] - Net::YAIL#silent= is deprecated as of 1.4.1 - .log can be used instead'
256
276
  @log_silent = val
257
277
  end
258
278
 
259
279
  def loud
260
- @log.warn '[DEPRECATED] - Net::YAIL#loud is deprecated as of 1.4.1'
280
+ @log.warn '[DEPRECATED] - Net::YAIL#loud is deprecated as of 1.4.1 - .log can be used instead'
261
281
  return @log_loud
262
282
  end
263
283
  def loud=(val)
264
- @log.warn '[DEPRECATED] - Net::YAIL#loud= is deprecated as of 1.4.1'
284
+ @log.warn '[DEPRECATED] - Net::YAIL#loud= is deprecated as of 1.4.1 - .log can be used instead'
265
285
  @log_loud = val
266
286
  end
267
287
 
@@ -309,6 +329,29 @@ class YAIL
309
329
  @password = options[:server_password]
310
330
  @ssl = options[:use_ssl] || false
311
331
 
332
+ #############################################
333
+ # TODO: DEPRECATED!!
334
+ #
335
+ # TODO: Delete this!
336
+ #############################################
337
+ @legacy_handlers = Hash.new
338
+
339
+ # Shared resources for threads to try and coordinate.... I know very
340
+ # little about thread safety, so this stuff may be a terrible disaster.
341
+ # Please send me better approaches if you are less stupid than I.
342
+ @input_buffer = []
343
+ @input_buffer_mutex = Mutex.new
344
+ @privmsg_buffer = {}
345
+ @privmsg_buffer_mutex = Mutex.new
346
+
347
+ # Buffered output is allowed to go out right away.
348
+ @next_message_time = Time.now
349
+
350
+ # Setup callback/filter hashes
351
+ @before_filters = Hash.new
352
+ @after_filters = Hash.new
353
+ @callback = Hash.new
354
+
312
355
  # Special handling to avoid mucking with Logger constants if we're using a different logger
313
356
  if options[:log]
314
357
  @log = options[:log]
@@ -331,36 +374,13 @@ class YAIL
331
374
  eventmap = "#{File.dirname(__FILE__)}/yail/eventmap.yml"
332
375
  @event_number_lookup = File.open(eventmap) { |file| YAML::load(file) }.invert
333
376
 
334
- # We're not dead... yet...
335
- @dead_socket = false
336
-
337
- # Build our socket - if something goes wrong, it's immediately a dead socket.
338
377
  if @io
339
378
  @socket = @io
340
379
  else
341
- begin
342
- @socket = TCPSocket.new(@address, @port)
343
- setup_ssl if @ssl
344
- rescue StandardError => boom
345
- @log.fatal "+++ERROR: Unable to open socket connection in Net::YAIL.initialize: #{boom.inspect}"
346
- @dead_socket = true
347
- raise
348
- end
380
+ prepare_tcp_socket
349
381
  end
350
382
 
351
- # Shared resources for threads to try and coordinate.... I know very
352
- # little about thread safety, so this stuff may be a terrible disaster.
353
- # Please send me better approaches if you are less stupid than I.
354
- @input_buffer = []
355
- @input_buffer_mutex = Mutex.new
356
- @privmsg_buffer = {}
357
- @privmsg_buffer_mutex = Mutex.new
358
-
359
- # Buffered output is allowed to go out right away.
360
- @next_message_time = Time.now
361
- # Setup handlers
362
- @handlers = Hash.new
363
- setup_default_handlers
383
+ set_defaults
364
384
  end
365
385
 
366
386
  # Starts listening for input and builds the perma-threads that check for
@@ -375,23 +395,19 @@ class YAIL
375
395
  # Exit a bit more gracefully than just crashing out - allow any :outgoing_quit filters to run,
376
396
  # and even give the server a second to clean up before we fry the connection
377
397
  #
378
- # TODO: perhaps this should be in a callback so user can override TERM/INT handling
398
+ # TODO: This REALLY doesn't belong here! This is saying everybody who uses the lib wants
399
+ # CTRL+C to end the app at the YAIL level. Not necessarily true outside bot-land.
379
400
  quithandler = lambda { quit('Terminated by user'); sleep 1; stop_listening; exit }
380
401
  trap("INT", quithandler)
381
402
  trap("TERM", quithandler)
382
403
 
383
- # Build forced / magic logic - welcome setting @me, ping response, etc.
384
- # Since we do these here, nobody can skip them and they're always first.
385
- setup_magic_handlers
386
-
387
404
  # Begin the listening thread
388
405
  @ioloop_thread = Thread.new {io_loop}
389
406
  @input_processor = Thread.new {process_input_loop}
390
407
  @privmsg_processor = Thread.new {process_privmsg_loop}
391
408
 
392
- # Let's begin the cycle by telling the server who we are. This should
393
- # start a TERRIBLE CHAIN OF EVENTS!!!
394
- handle(:outgoing_begin_connection, @username, @address, @realname)
409
+ # Let's begin the cycle by telling the server who we are. This should start a TERRIBLE CHAIN OF EVENTS!!!
410
+ dispatch OutgoingEvent.new(:type => :begin_connection, :username => @username, :address => @address, :realname => @realname)
395
411
  end
396
412
 
397
413
  # This starts the connection, threading, etc. as start_listening, but *forces* the user into
@@ -428,6 +444,71 @@ class YAIL
428
444
 
429
445
  private
430
446
 
447
+ # Sets up all default filters and callbacks
448
+ def set_defaults
449
+ # Set up callbacks for slightly more important things than reporting - note that these should
450
+ # eventually be changed as they don't belong in the core of YAIL. Note that since these are
451
+ # callbacks, the user can very easily overwrite them, at least.
452
+ on_nicknameinuse self.method(:_nicknameinuse)
453
+ on_namreply self.method(:_namreply)
454
+
455
+ # Set up truly core handlers/filters - these shouldn't be overridden unless users like to get
456
+ # their hands dirty
457
+ set_callback(:outgoing_begin_connection, self.method(:out_begin_connection))
458
+ on_ping self.method(:magic_ping)
459
+
460
+ # Nick change magically setting @me is necessary as a filter - user can handle the event and do
461
+ # anything he wants, but this should still run.
462
+ hearing_nick self.method(:magic_nick)
463
+
464
+ # Welcome magic also sets @me magically, so it's a filter
465
+ hearing_welcome self.method(:magic_welcome)
466
+
467
+ # Outgoing handlers are what make this app actually work - users who override these have to
468
+ # do so very explicitly (no "on_xxx" magic) and will probably break stuff. Use filters instead!
469
+
470
+ # These three need magic to buffer their output, so can't use our simpler create_command system
471
+ set_callback :outgoing_msg, self.method(:magic_out_msg)
472
+ set_callback :outgoing_ctcp, self.method(:magic_out_ctcp)
473
+ set_callback :outgoing_act, self.method(:magic_out_act)
474
+
475
+ # All PRIVMSG events eventually hit this - it's a legacy thing, and kinda dumb, but there you
476
+ # have it. Just sends a raw PRIVMSG out to the socket.
477
+ create_command :privmsg, "PRIVMSG :target ::message", :target, :message
478
+
479
+ # The rest of these should be fairly obvious
480
+ create_command :notice, "NOTICE :target ::message", :target, :message
481
+ create_command :ctcpreply, "NOTICE :target :\001:message\001", :target, :message
482
+ create_command :mode, "MODE", :target, " :target", :modes, " :modes", :objects, " :objects"
483
+ create_command :join, "JOIN :channel", :channel, :password, " :password"
484
+ create_command :part, "PART :channel", :channel, :message, " ::message"
485
+ create_command :quit, "QUIT", :message, " ::message"
486
+ create_command :nick, "NICK ::nick", :nick
487
+ create_command :user, "USER :username :hostname :servername ::realname", :username, :hostname, :servername, :realname
488
+ create_command :pass, "PASS :password", :password
489
+ create_command :oper, "OPER :user :password", :user, :password
490
+ create_command :topic, "TOPIC :channel", :channel, :topic, " ::topic"
491
+ create_command :names, "NAMES", :channel, " :channel"
492
+ create_command :list, "LIST", :channel, " :channel", :server, " :server"
493
+ create_command :invite, "INVITE :nick :channel", :nick, :channel
494
+ create_command :kick, "KICK :channel :nick", :nick, :channel, :message, " ::message"
495
+ end
496
+
497
+ # Prepares @socket for use and defaults @dead_socket to false
498
+ def prepare_tcp_socket
499
+ @dead_socket = false
500
+
501
+ # Build our socket - if something goes wrong, it's immediately a dead socket.
502
+ begin
503
+ @socket = TCPSocket.new(@address, @port)
504
+ setup_ssl if @ssl
505
+ rescue StandardError => boom
506
+ @log.fatal "+++ERROR: Unable to open socket connection in Net::YAIL.initialize: #{boom.inspect}"
507
+ @dead_socket = true
508
+ raise
509
+ end
510
+ end
511
+
431
512
  # If user asked for SSL, this is where we set it all up
432
513
  def setup_ssl
433
514
  require 'openssl'
@@ -480,7 +561,7 @@ class YAIL
480
561
  end
481
562
 
482
563
  # This should be called from a thread only! Does nothing but listens
483
- # forever for incoming data, and calling handlers due to this listening
564
+ # forever for incoming data, and calling filters/callback due to this listening
484
565
  def io_loop
485
566
  loop do
486
567
  # Possible fix for SSL one-message-behind issue from BP - thanks!
@@ -515,11 +596,12 @@ class YAIL
515
596
  @input_buffer.clear
516
597
  end
517
598
 
518
- if (lines)
599
+ if lines
519
600
  # Now actually handle the data we copied, secure in the knowledge
520
601
  # that our reader thread is no longer going to wait on us.
521
- while lines.empty? == false
522
- process_input(lines.shift)
602
+ until lines.empty?
603
+ event = Net::YAIL::IncomingEvent.parse(lines.shift)
604
+ dispatch(event)
523
605
  end
524
606
 
525
607
  lines = nil
@@ -530,9 +612,9 @@ class YAIL
530
612
  end
531
613
 
532
614
  # Grabs one message for each target in the private message buffer, removing
533
- # messages from @privmsg_buffer. Returns a hash array of target -> text
615
+ # messages from @privmsg_buffer. Returns an array of events to process
534
616
  def pop_privmsgs
535
- privmsgs = {}
617
+ privmsgs = []
536
618
 
537
619
  # Only synchronize long enough to pop the appropriate messages. By
538
620
  # the way, this is UGLY! I should really move some of this stuff....
@@ -545,23 +627,18 @@ class YAIL
545
627
  next
546
628
  end
547
629
 
548
- privmsgs[target] = @privmsg_buffer[target].shift
630
+ privmsgs.push @privmsg_buffer[target].shift
549
631
  end
550
632
  end
551
633
 
552
634
  return privmsgs
553
635
  end
554
636
 
555
- # Checks for new private messages, and outputs all that are gathered from
556
- # pop_privmsgs, if any
637
+ # Checks for new private messages, and dispatches all that are gathered from pop_privmsgs, if any
557
638
  def check_privmsg_output
558
639
  privmsgs = pop_privmsgs
559
640
  @next_message_time = Time.now + @throttle_seconds unless privmsgs.empty?
560
-
561
- for (target, out_array) in privmsgs
562
- report(out_array[1]) unless out_array[1].to_s.empty?
563
- raw("PRIVMSG #{target} :#{out_array.first}", false)
564
- end
641
+ privmsgs.each {|event| dispatch event}
565
642
  end
566
643
 
567
644
  # Our final thread loop - grabs the first privmsg for each target and
@@ -574,158 +651,135 @@ class YAIL
574
651
  end
575
652
  end
576
653
 
577
- # Gets some input, sends stuff off to a handler. Yay.
578
- def process_input(line)
579
- # Allow global handler to break the chain, filter the line, whatever. For
580
- # this release, it's a hack. 2.0 will be better.
581
- if (Array === @handlers[:incoming_any])
582
- for handler in @handlers[:incoming_any]
583
- result = handler.call(line)
584
- return if result == true
585
- end
586
- end
587
-
588
- # Use the exciting new-new parser
589
- event = Net::YAIL::IncomingEvent.parse(line)
590
-
591
- # Partial conversion to using events - we still have a horrible case statement, but
592
- # we're at least using the event object. Slightly less hacky than before.
593
-
594
- # Except for this - we still have to handle numerics the crappy way until we build the proper
595
- # dispatching of events
596
- event = event.parent if event.parent && :incoming_numeric == event.parent.type
597
-
598
- case event.type
599
- # Ping is important to handle quickly, so it comes first.
600
- when :incoming_ping
601
- handle(event.type, event.text)
602
-
603
- when :incoming_numeric
604
- # Lovely - I passed in a "nick" - which, according to spec, is NEVER part of a numeric reply
605
- handle_numeric(event.numeric, event.servername, nil, event.target, event.text)
606
-
607
- when :incoming_invite
608
- handle(event.type, event.fullname, event.nick, event.channel)
654
+ ##################################################
655
+ # EVENT HANDLING ULTRA SUPERSYSTEM DELUXE!!!
656
+ ##################################################
609
657
 
610
- # Fortunately, the legacy handler for all five "message" types is the same!
611
- when :incoming_msg, :incoming_ctcp, :incoming_act, :incoming_notice, :incoming_ctcpreply
612
- # Legacy handling requires merger of target and channel....
613
- target = event.target if event.pm?
614
- target = event.channel if !target
658
+ public
659
+ # Prepends the given block or method to the before_filters array for the given type. Before-filters are called
660
+ # before the event callback has run, and can stop the event (and other filters) from running by calling the event's
661
+ # end_chain() method. Filters shouldn't do this very often! Before-filtering can modify output text before the
662
+ # event callback runs, ignore incoming events for a given user, etc.
663
+ def before_filter(event_type, method = nil, &block)
664
+ filter = block_given? ? block : method
665
+ if filter
666
+ event_type = numeric_event_type_convert(event_type)
667
+ @before_filters[event_type] ||= Array.new
668
+ @before_filters[event_type].unshift(filter)
669
+ end
670
+ end
615
671
 
616
- # Notices come from server sometimes, so... another merger for legacy fun!
617
- nick = event.server? ? '' : event.nick
618
- handle(event.type, event.from, nick, target, event.text)
672
+ # Sets up the callback for the given incoming event type. Note that unlike Net::YAIL 1.4.x and prior, there is no
673
+ # longer a concept of multiple callbacks! Use filters for that kind of functionality. Think this way: the callback
674
+ # is the action that takes place when an event hits. Filters are for functionality related to the event, but not
675
+ # the definitive callback - logging, filtering messages, stats gathering, ignoring messages from a set user, etc.
676
+ def set_callback(event_type, method = nil, &block)
677
+ callback = block_given? ? block : method
678
+ event_type = numeric_event_type_convert(event_type)
679
+ @callback[event_type] = callback
680
+ @callback.delete(event_type) unless callback
681
+ end
619
682
 
620
- # This is a bit painful for right now - just use some hacks to make it work semi-nicely,
621
- # but let's not put hacks into the core Event object. Modes need reworking soon anyway.
622
- #
623
- # NOTE: text is currently the mode settings ('+b', for instance) - very bad. TODO: FIX FIX FIX!
624
- when :incoming_mode
625
- # Modes can come from the server, so legacy system again regularly sent nil data....
626
- nick = event.server? ? '' : event.nick
627
- handle(event.type, event.from, nick, event.channel, event.text, event.targets.join(' '))
683
+ # Prepends the given block or method to the after_filters array for the given type. After-filters are called after
684
+ # the event callback has run, and cannot stop other after-filters from running. Best used for logging or statistics
685
+ # gathering.
686
+ def after_filter(event_type, method = nil, &block)
687
+ filter = block_given? ? block : method
688
+ if filter
689
+ event_type = numeric_event_type_convert(event_type)
690
+ @after_filters[event_type] ||= Array.new
691
+ @after_filters[event_type].unshift(filter)
692
+ end
693
+ end
628
694
 
629
- when :incoming_topic_change
630
- handle(event.type, event.fullname, event.nick, event.channel, event.text)
695
+ # Reports may not get printed in the proper order since I scrubbed the
696
+ # IRCSocket report capturing, but this is way more straightforward to me.
697
+ def report(*lines)
698
+ lines.each {|line| $stdout.puts "(#{Time.now.strftime('%H:%M.%S')}) #{line}"}
699
+ end
631
700
 
632
- when :incoming_join
633
- handle(event.type, event.fullname, event.nick, event.channel)
701
+ # Converts events that are numerics into the internal "incoming_numeric_xxx" format
702
+ def numeric_event_type_convert(type)
703
+ if (type.to_s =~ /^incoming_(.*)$/)
704
+ number = @event_number_lookup[$1].to_i
705
+ type = :"incoming_numeric_#{number}" if number > 0
706
+ end
634
707
 
635
- when :incoming_part
636
- handle(event.type, event.fullname, event.nick, event.channel, event.text)
708
+ return type
709
+ end
637
710
 
638
- when :incoming_kick
639
- handle(event.type, event.fullname, event.nick, event.channel, event.target, event.text)
711
+ # Given an event, calls pre-callback filters, callback, and post-callback filters. Uses hacky
712
+ # :incoming_any event if event object is of IncomingEvent type.
713
+ def dispatch(event)
714
+ # Add all before-callback stuff to our chain
715
+ chain = []
716
+ chain.push @before_filters[:incoming_any] if Net::YAIL::IncomingEvent === event
717
+ chain.push @before_filters[:outgoing_any] if Net::YAIL::OutgoingEvent === event
718
+ chain.push @before_filters[event.type]
719
+ chain.flatten!
720
+ chain.compact!
721
+
722
+ # Run each filter in the chain, exiting early if event was handled
723
+ for filter in chain
724
+ filter.call(event)
725
+ return if event.handled?
726
+ end
640
727
 
641
- when :incoming_quit
642
- handle(event.type, event.fullname, event.nick, event.text)
728
+ # Legacy handler - return if true, since that's how the old system works - EXCEPTION for outgoing events, since
729
+ # the old system didn't allow the outgoing "core" code to be skipped!
730
+ if true == legacy_process_event(event)
731
+ return unless Net::YAIL::OutgoingEvent === event
732
+ end
643
733
 
644
- when :incoming_nick
645
- handle(event.type, event.fullname, event.nick, event.text)
734
+ # Add new callback and all after-callback stuff to a new chain
735
+ chain = []
736
+ chain.push @callback[event.type]
737
+ chain.push @after_filters[event.type]
738
+ chain.push @after_filters[:incoming_any] if Net::YAIL::IncomingEvent === event
739
+ chain.push @after_filters[:outgoing_any] if Net::YAIL::OutgoingEvent === event
740
+ chain.flatten!
741
+ chain.compact!
742
+
743
+ # Run all after-filters blindly - none can affect callback, so after-filters can't set handled to true
744
+ chain.each {|filter| filter.call(event)}
745
+ end
646
746
 
647
- when :incoming_error
648
- handle(event.type, event.text)
747
+ # Handles magic listener setup methods: on_xxx, hearing_xxx, heard_xxx, saying_xxx, and said_xxx
748
+ def method_missing(name, *args, &block)
749
+ method = nil
750
+ event_type = nil
649
751
 
650
- # Unknown line!
651
- else
652
- # This should really never happen, but isn't technically an error per se
653
- @log.warn 'Unknown line: %s!' % line.inspect
654
- handle(:incoming_miscellany, line)
655
- end
656
- end
752
+ case name.to_s
753
+ when /^on_(.*)$/
754
+ method = :set_callback
755
+ event_type = :"incoming_#{$1}"
657
756
 
658
- ##################################################
659
- # EVENT HANDLING ULTRA SUPERSYSTEM DELUXE!!!
660
- ##################################################
757
+ when /^hearing_(.*)$/
758
+ method = :before_filter
759
+ event_type = :"incoming_#{$1}"
661
760
 
662
- public
663
- # Event handler hook. Kinda hacky. Calls your event(s) before the default
664
- # event. Default stuff will happen if your handler doesn't return true.
665
- def prepend_handler(event, *procs, &block)
666
- raise "Cannot change handlers while threads are listening!" if @ioloop_thread
667
-
668
- # Allow blocks as well as procs
669
- if block_given?
670
- procs.push(block)
671
- end
761
+ when /^heard_(.*)$/
762
+ method = :after_filter
763
+ event_type = :"incoming_#{$1}"
672
764
 
673
- # See if this is a word for a numeric - only applies to incoming events
674
- if (event.to_s =~ /^incoming_(.*)$/)
675
- number = @event_number_lookup[$1].to_i
676
- event = :"incoming_numeric_#{number}" if number > 0
677
- end
765
+ when /^saying_(.*)$/
766
+ method = :before_filter
767
+ event_type = :"outgoing_#{$1}"
678
768
 
679
- @handlers[event] ||= Array.new
680
- until procs.empty?
681
- @handlers[event].unshift(procs.pop)
769
+ when /^said_(.*)$/
770
+ method = :after_filter
771
+ event_type = :"outgoing_#{$1}"
682
772
  end
683
- end
684
773
 
685
- # Handles the given event (if it's in the @handlers array) with the
686
- # arguments specified.
687
- #
688
- # The @handlers must be a hash where key = event to handle and value is
689
- # a Proc object (via Class.method(:name) or just proc {...}).
690
- # This should be fine if you're setting up handlers with the prepend_handler
691
- # method, but if you get "clever," you're on your own.
692
- def handle(event, *arguments)
693
- # Don't bother with anything if there are no handlers registered.
694
- return unless Array === @handlers[event]
695
-
696
- @log.debug "+++EVENT HANDLER: Handling event #{event} via #{@handlers[event].inspect}:"
697
-
698
- # Call all hooks in order until one breaks the chain. For incoming
699
- # events, we want something to break the chain or else it'll likely
700
- # hit a reporter. For outgoing events, we tend to report them anyway,
701
- # so no need to worry about ending the chain except when the bot wants
702
- # to take full control over them.
703
- result = false
704
- for handler in @handlers[event]
705
- result = handler.call(*arguments)
706
- break if result == true
707
- end
708
- end
774
+ # Magic methods MUST have an arg or a block!
775
+ filter_or_callback_method = block_given? ? block : args.shift
709
776
 
710
- # Since numerics are so many and so varied, this method will auto-fallback
711
- # to a simple report if no handler was defined.
712
- def handle_numeric(number, fullactor, actor, target, text)
713
- # All numerics share the same args, and rarely care about anything but
714
- # text, so let's make it easier by passing a hash instead of a list
715
- args = {:fullactor => fullactor, :actor => actor, :target => target}
716
- base_event = :"incoming_numeric_#{number}"
717
- if Array === @handlers[base_event]
718
- handle(base_event, text, args)
719
- else
720
- # No handler = report and don't worry about it
721
- @log.info "Unknown raw #{number.to_s} from #{fullactor}: #{text}"
722
- end
723
- end
777
+ # If we didn't match a magic method signature, or we don't have the expected parameters, call
778
+ # parent's method_missing. Just to be safe, we also return, in case YAIL one day subclasses
779
+ # from something that handles some method_missing stuff.
780
+ return super if method.nil? || event_type.nil? || args.length > 0
724
781
 
725
- # Reports may not get printed in the proper order since I scrubbed the
726
- # IRCSocket report capturing, but this is way more straightforward to me.
727
- def report(*lines)
728
- lines.each {|line| $stdout.puts "(#{Time.now.strftime('%H:%M.%S')}) #{line}"}
782
+ self.send(method, event_type, filter_or_callback_method)
729
783
  end
730
784
  end
731
785