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