net-yail 1.4.6 → 1.5.1

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.
@@ -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