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.
- data/examples/logger/logger_bot.rb +20 -21
- data/examples/loudbot/bot_runner.rb +114 -0
- data/examples/loudbot/loudbot.rb +87 -0
- data/examples/loudbot/shuffle.rb +17 -0
- data/examples/simple/dumbbot.rb +9 -4
- data/lib/net/yail.rb +437 -383
- data/lib/net/yail/default_events.rb +5 -163
- data/lib/net/yail/event.rb +65 -50
- data/lib/net/yail/irc_bot.rb +7 -7
- data/lib/net/yail/legacy_events.rb +214 -0
- data/lib/net/yail/magic_events.rb +31 -27
- data/lib/net/yail/output_api.rb +82 -262
- data/lib/net/yail/report_events.rb +173 -0
- data/lib/net/yail/yail-version.rb +1 -1
- data/tests/mock_irc.rb +13 -3
- data/tests/tc_event.rb +28 -28
- data/tests/tc_yail.rb +93 -32
- metadata +21 -11
- data/YAIL-RDOC +0 -51
@@ -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.
|
46
|
-
@irc.
|
47
|
-
@irc.
|
48
|
-
@irc.
|
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(
|
54
|
+
def _in_msg(event)
|
56
55
|
# check if this is a /msg command, or normal channel talk
|
57
|
-
if
|
58
|
-
incoming_private_message(
|
56
|
+
if event.pm?
|
57
|
+
incoming_private_message(event.nick, event.message)
|
59
58
|
else
|
60
|
-
incoming_channel_message(
|
59
|
+
incoming_channel_message(event.nick, event.channel, event.message)
|
61
60
|
end
|
62
61
|
end
|
63
62
|
|
64
|
-
def _in_act(
|
63
|
+
def _in_act(event)
|
65
64
|
# check if this is a /msg command, or normal channel talk
|
66
|
-
return if
|
67
|
-
log_channel_message(
|
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(
|
140
|
-
join
|
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(
|
145
|
-
if
|
143
|
+
def _in_kick(event)
|
144
|
+
if event.target == bot_name
|
146
145
|
# Rejoin almost immediately - logging is important.
|
147
|
-
join
|
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(
|
155
|
-
key = @passwords[
|
156
|
-
|
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
|
data/examples/simple/dumbbot.rb
CHANGED
@@ -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.
|
22
|
-
irc.
|
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!
|
data/lib/net/yail.rb
CHANGED
@@ -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
|
-
#
|
37
|
-
#
|
38
|
-
#
|
39
|
-
#
|
40
|
-
#
|
41
|
-
#
|
42
|
-
#
|
43
|
-
#
|
44
|
-
#
|
45
|
-
#
|
46
|
-
# *
|
47
|
-
#
|
48
|
-
#
|
49
|
-
#
|
50
|
-
#
|
51
|
-
#
|
52
|
-
#
|
53
|
-
#
|
54
|
-
#
|
55
|
-
#
|
56
|
-
#
|
57
|
-
#
|
58
|
-
#
|
59
|
-
#
|
60
|
-
#
|
61
|
-
# *
|
62
|
-
#
|
63
|
-
# *
|
64
|
-
#
|
65
|
-
#
|
66
|
-
#
|
67
|
-
#
|
68
|
-
# *
|
69
|
-
#
|
70
|
-
#
|
71
|
-
#
|
72
|
-
# *
|
73
|
-
#
|
74
|
-
# *
|
75
|
-
#
|
76
|
-
# *
|
77
|
-
#
|
78
|
-
#
|
79
|
-
#
|
80
|
-
#
|
81
|
-
#
|
82
|
-
#
|
83
|
-
#
|
84
|
-
#
|
85
|
-
#
|
86
|
-
#
|
87
|
-
#
|
88
|
-
#
|
89
|
-
#
|
90
|
-
#
|
91
|
-
#
|
92
|
-
#
|
93
|
-
#
|
94
|
-
#
|
95
|
-
#
|
96
|
-
#
|
97
|
-
#
|
98
|
-
#
|
99
|
-
#
|
100
|
-
#
|
101
|
-
#
|
102
|
-
#
|
103
|
-
#
|
104
|
-
#
|
105
|
-
# *
|
106
|
-
#
|
107
|
-
#
|
108
|
-
#
|
109
|
-
#
|
110
|
-
# *
|
111
|
-
#
|
112
|
-
# *
|
113
|
-
#
|
114
|
-
#
|
115
|
-
#
|
116
|
-
#
|
117
|
-
#
|
118
|
-
#
|
119
|
-
#
|
120
|
-
#
|
121
|
-
#
|
122
|
-
#
|
123
|
-
#
|
124
|
-
#
|
125
|
-
#
|
126
|
-
#
|
127
|
-
#
|
128
|
-
#
|
129
|
-
#
|
130
|
-
# *
|
131
|
-
#
|
132
|
-
#
|
133
|
-
#
|
134
|
-
#
|
135
|
-
#
|
136
|
-
#
|
137
|
-
#
|
138
|
-
#
|
139
|
-
#
|
140
|
-
#
|
141
|
-
#
|
142
|
-
#
|
143
|
-
# will
|
144
|
-
#
|
145
|
-
#
|
146
|
-
#
|
147
|
-
#
|
148
|
-
#
|
149
|
-
#
|
150
|
-
#
|
151
|
-
#
|
152
|
-
#
|
153
|
-
#
|
154
|
-
#
|
155
|
-
#
|
156
|
-
#
|
157
|
-
#
|
158
|
-
#
|
159
|
-
#
|
160
|
-
#
|
161
|
-
#
|
162
|
-
#
|
163
|
-
#
|
164
|
-
#
|
165
|
-
#
|
166
|
-
#
|
167
|
-
#
|
168
|
-
#
|
169
|
-
#
|
170
|
-
#
|
171
|
-
#
|
172
|
-
#
|
173
|
-
#
|
174
|
-
#
|
175
|
-
#
|
176
|
-
#
|
177
|
-
#
|
178
|
-
#
|
179
|
-
#
|
180
|
-
#
|
181
|
-
#
|
182
|
-
#
|
183
|
-
#
|
184
|
-
#
|
185
|
-
#
|
186
|
-
#
|
187
|
-
#
|
188
|
-
#
|
189
|
-
#
|
190
|
-
#
|
191
|
-
#
|
192
|
-
#
|
193
|
-
#
|
194
|
-
#
|
195
|
-
#
|
196
|
-
#
|
197
|
-
#
|
198
|
-
#
|
199
|
-
#
|
200
|
-
#
|
201
|
-
#
|
202
|
-
#
|
203
|
-
#
|
204
|
-
#
|
205
|
-
#
|
206
|
-
#
|
207
|
-
#
|
208
|
-
#
|
209
|
-
#
|
210
|
-
#
|
211
|
-
#
|
212
|
-
#
|
213
|
-
#
|
214
|
-
#
|
215
|
-
#
|
216
|
-
#
|
217
|
-
#
|
218
|
-
#
|
219
|
-
#
|
220
|
-
#
|
221
|
-
#
|
222
|
-
#
|
223
|
-
#
|
224
|
-
#
|
225
|
-
#
|
226
|
-
#
|
227
|
-
#
|
228
|
-
#
|
229
|
-
#
|
230
|
-
#
|
231
|
-
#
|
232
|
-
#
|
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
|
-
|
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
|
-
|
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:
|
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
|
-
|
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
|
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
|
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
|
-
|
522
|
-
|
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
|
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
|
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
|
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
|
-
|
578
|
-
|
579
|
-
|
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
|
-
|
611
|
-
|
612
|
-
|
613
|
-
|
614
|
-
|
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
|
-
|
617
|
-
|
618
|
-
|
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
|
-
|
621
|
-
|
622
|
-
|
623
|
-
|
624
|
-
|
625
|
-
|
626
|
-
|
627
|
-
|
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
|
-
|
630
|
-
|
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
|
-
|
633
|
-
|
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
|
-
|
636
|
-
|
708
|
+
return type
|
709
|
+
end
|
637
710
|
|
638
|
-
|
639
|
-
|
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
|
-
|
642
|
-
|
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
|
-
|
645
|
-
|
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
|
-
|
648
|
-
|
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
|
-
|
651
|
-
|
652
|
-
|
653
|
-
|
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
|
-
|
660
|
-
|
757
|
+
when /^hearing_(.*)$/
|
758
|
+
method = :before_filter
|
759
|
+
event_type = :"incoming_#{$1}"
|
661
760
|
|
662
|
-
|
663
|
-
|
664
|
-
|
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
|
-
|
674
|
-
|
675
|
-
|
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
|
-
|
680
|
-
|
681
|
-
|
769
|
+
when /^said_(.*)$/
|
770
|
+
method = :after_filter
|
771
|
+
event_type = :"outgoing_#{$1}"
|
682
772
|
end
|
683
|
-
end
|
684
773
|
|
685
|
-
|
686
|
-
|
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
|
-
|
711
|
-
|
712
|
-
|
713
|
-
|
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
|
-
|
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
|
|