newton 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +22 -0
- data/README.md +189 -0
- data/Rakefile +6 -0
- data/examples/autovoice.rb +45 -0
- data/examples/echo_bot.rb +22 -0
- data/examples/excess_flood.rb +23 -0
- data/examples/memo.rb +50 -0
- data/examples/schema.rb +41 -0
- data/examples/secure_eval.rb +46 -0
- data/lib/newton/ban.rb +40 -0
- data/lib/newton/bot.rb +361 -0
- data/lib/newton/callback.rb +24 -0
- data/lib/newton/channel.rb +362 -0
- data/lib/newton/constants.rb +123 -0
- data/lib/newton/exceptions.rb +25 -0
- data/lib/newton/formatted_logger.rb +64 -0
- data/lib/newton/irc.rb +261 -0
- data/lib/newton/isupport.rb +96 -0
- data/lib/newton/mask.rb +46 -0
- data/lib/newton/message.rb +162 -0
- data/lib/newton/message_queue.rb +62 -0
- data/lib/newton/rubyext/infinity.rb +1 -0
- data/lib/newton/rubyext/module.rb +18 -0
- data/lib/newton/rubyext/queue.rb +19 -0
- data/lib/newton/rubyext/string.rb +24 -0
- data/lib/newton/syncable.rb +55 -0
- data/lib/newton/user.rb +226 -0
- data/lib/newton.rb +1 -0
- data/test/helper.rb +60 -0
- data/test/test_commands.rb +85 -0
- data/test/test_events.rb +89 -0
- data/test/test_helpers.rb +14 -0
- data/test/test_irc.rb +38 -0
- data/test/test_message.rb +117 -0
- data/test/test_parse.rb +153 -0
- data/test/test_queue.rb +49 -0
- data/test/tests.rb +9 -0
- metadata +100 -0
data/lib/newton/irc.rb
ADDED
@@ -0,0 +1,261 @@
|
|
1
|
+
module Newton
|
2
|
+
class IRC
|
3
|
+
# @return [ISupport]
|
4
|
+
attr_reader :isupport
|
5
|
+
def initialize(bot, config)
|
6
|
+
@bot, @config = bot, config
|
7
|
+
@isupport = ISupport.new
|
8
|
+
end
|
9
|
+
|
10
|
+
# Establish a connection.
|
11
|
+
#
|
12
|
+
# @return [void]
|
13
|
+
def connect
|
14
|
+
@registration = []
|
15
|
+
|
16
|
+
@whois_updates = Hash.new {|h, k| h[k] = {}}
|
17
|
+
@in_lists = Set.new
|
18
|
+
|
19
|
+
tcp_socket = TCPSocket.open(@config.server, @config.port)
|
20
|
+
|
21
|
+
if @config.ssl
|
22
|
+
require 'openssl'
|
23
|
+
|
24
|
+
ssl_context = OpenSSL::SSL::SSLContext.new
|
25
|
+
ssl_context.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
26
|
+
|
27
|
+
FormattedLogger.debug "Using SSL with #{@config.server}:#{@config.port}"
|
28
|
+
|
29
|
+
@socket = OpenSSL::SSL::SSLSocket.new(tcp_socket, ssl_context)
|
30
|
+
@socket.sync = true
|
31
|
+
@socket.connect
|
32
|
+
else
|
33
|
+
@socket = tcp_socket
|
34
|
+
end
|
35
|
+
|
36
|
+
@queue = MessageQueue.new(@socket, @bot)
|
37
|
+
message "PASS #{@config.password}" if @config.password
|
38
|
+
message "NICK #{@config.nick}"
|
39
|
+
message "USER #{@config.nick} 0 * :#{@config.realname}"
|
40
|
+
|
41
|
+
Thread.new do
|
42
|
+
while line = @socket.gets
|
43
|
+
parse line
|
44
|
+
end
|
45
|
+
|
46
|
+
@bot.dispatch(:disconnect)
|
47
|
+
end
|
48
|
+
|
49
|
+
@queue.process!
|
50
|
+
end
|
51
|
+
|
52
|
+
# @api private
|
53
|
+
# @return [void]
|
54
|
+
def parse(input)
|
55
|
+
FormattedLogger.log(input, :incoming) if @bot.config.verbose
|
56
|
+
msg = Message.new(input, @bot)
|
57
|
+
events = []
|
58
|
+
dispatch_msg = nil
|
59
|
+
|
60
|
+
if msg.command == ERR_NICKNAMEINUSE.to_s
|
61
|
+
@bot.nick = msg.params[1] + "_"
|
62
|
+
end
|
63
|
+
|
64
|
+
if ("001".."004").include? msg.command
|
65
|
+
@registration << msg.command
|
66
|
+
if registered?
|
67
|
+
events << :connect
|
68
|
+
end
|
69
|
+
elsif msg.command == "PRIVMSG"
|
70
|
+
events.concat(if msg.ctcp?
|
71
|
+
[:ctcp]
|
72
|
+
elsif msg.channel?
|
73
|
+
[:message, :channel]
|
74
|
+
else
|
75
|
+
[:message, :private]
|
76
|
+
end)
|
77
|
+
|
78
|
+
dispatch_msg = msg
|
79
|
+
elsif msg.command == "PING"
|
80
|
+
events << :ping
|
81
|
+
message "PONG :#{msg.params.first}"
|
82
|
+
else
|
83
|
+
if msg.command == "005"
|
84
|
+
require 'pp'
|
85
|
+
@isupport.parse(*msg.params[1..-2].map {|v| v.split(" ")}.flatten)
|
86
|
+
elsif [RPL_TOPIC.to_s, RPL_NOTOPIC.to_s, "TOPIC"].include?(msg.command)
|
87
|
+
topic = case msg.command
|
88
|
+
when RPL_TOPIC.to_s
|
89
|
+
msg.params[2]
|
90
|
+
when "TOPIC"
|
91
|
+
msg.params[1]
|
92
|
+
else
|
93
|
+
""
|
94
|
+
end
|
95
|
+
msg.channel.sync(:topic, topic)
|
96
|
+
elsif msg.command == "JOIN"
|
97
|
+
if msg.user == @bot
|
98
|
+
msg.channel.sync_modes
|
99
|
+
end
|
100
|
+
msg.channel.add_user(msg.user)
|
101
|
+
elsif msg.command == "PART"
|
102
|
+
msg.channel.remove_user(msg.user)
|
103
|
+
elsif msg.command == "KICK"
|
104
|
+
msg.channel.remove_user(User.find_ensured(msg.params[1], @bot))
|
105
|
+
elsif msg.command == "KILL"
|
106
|
+
user = User.find_ensured(msg.params[1], @bot)
|
107
|
+
Channel.all.each do |channel|
|
108
|
+
channel.remove_user(user)
|
109
|
+
end
|
110
|
+
elsif msg.command == "QUIT"
|
111
|
+
Channel.all.each do |channel|
|
112
|
+
channel.remove_user(msg.user)
|
113
|
+
end
|
114
|
+
msg.user.synced = false
|
115
|
+
elsif msg.command == "NICK"
|
116
|
+
if msg.user == @bot
|
117
|
+
@bot.config.nick = msg.params.last
|
118
|
+
end
|
119
|
+
|
120
|
+
msg.user.nick = msg.params.last
|
121
|
+
elsif msg.command == "MODE"
|
122
|
+
msg.channel.sync_modes if msg.channel?
|
123
|
+
elsif msg.command == RPL_CHANNELMODEIS.to_s
|
124
|
+
modes = {}
|
125
|
+
arguments = msg.params[3..-1]
|
126
|
+
msg.params[2][1..-1].split("").each do |mode|
|
127
|
+
if (@isupport["CHANMODES"]["B"] + @isupport["CHANMODES"]["C"]).include?(mode)
|
128
|
+
modes[mode] = arguments.shift
|
129
|
+
else
|
130
|
+
modes[mode] = true
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
msg.channel.sync(:modes, modes, false)
|
135
|
+
elsif msg.command == RPL_NAMEREPLY.to_s
|
136
|
+
unless @in_lists.include?(:names)
|
137
|
+
msg.channel.clear_users
|
138
|
+
end
|
139
|
+
@in_lists << :names
|
140
|
+
|
141
|
+
msg.params[3].split(" ").each do |user|
|
142
|
+
if @isupport["PREFIX"].values.include?(user[0..0])
|
143
|
+
prefix = user[0..0]
|
144
|
+
nick = user[1..-1]
|
145
|
+
else
|
146
|
+
nick = user
|
147
|
+
prefix = nil
|
148
|
+
end
|
149
|
+
user = User.find_ensured(nick, @bot)
|
150
|
+
msg.channel.add_user(user, prefix)
|
151
|
+
end
|
152
|
+
|
153
|
+
elsif msg.command == RPL_WHOISUSER.to_s
|
154
|
+
user = User.find_ensured(msg.params[1], @bot)
|
155
|
+
@whois_updates[user].merge!({
|
156
|
+
:user => msg.params[2],
|
157
|
+
:host => msg.params[3],
|
158
|
+
:realname => msg.params[5],
|
159
|
+
})
|
160
|
+
elsif msg.command == RPL_WHOISACCOUNT.to_s
|
161
|
+
user = User.find_ensured(msg.params[1], @bot)
|
162
|
+
authname = msg.params[2]
|
163
|
+
@whois_updates[user].merge!({:authname => authname})
|
164
|
+
elsif msg.command == RPL_WHOISCHANNELS.to_s
|
165
|
+
user = User.find_ensured(msg.params[1], @bot)
|
166
|
+
channels = msg.params[2].scan(/#{@isupport["CHANTYPES"].join}[^ ]+/o).map {|c| Channel.find_ensured(c, @bot) }
|
167
|
+
user.sync(:channels, channels, true)
|
168
|
+
elsif msg.command == RPL_WHOISIDLE.to_s
|
169
|
+
user = User.find_ensured(msg.params[1], @bot)
|
170
|
+
@whois_updates[user].merge!({
|
171
|
+
:idle => msg.params[2].to_i,
|
172
|
+
:signed_on_at => Time.at(msg.params[3].to_i),
|
173
|
+
})
|
174
|
+
elsif msg.command == "671"
|
175
|
+
user = User.find_ensured(msg.params[1], @bot)
|
176
|
+
@whois_updates[user].merge!({:secure? => true})
|
177
|
+
elsif msg.command == ERR_NOSUCHSERVER.to_s
|
178
|
+
if user = User.find(msg.params[1]) # not _ensured, we only want a user that already exists
|
179
|
+
user.sync(:unknown?, true, true)
|
180
|
+
@whois_updates.delete user
|
181
|
+
# TODO freenode specific, test on other IRCd
|
182
|
+
end
|
183
|
+
elsif msg.command == ERR_NOSUCHNICK.to_s
|
184
|
+
user = User.find_ensured(msg.params[1], @bot)
|
185
|
+
user.sync(:unknown?, true, true)
|
186
|
+
elsif msg.command == RPL_ENDOFWHOIS.to_s
|
187
|
+
user = User.find_ensured(msg.params[1], @bot)
|
188
|
+
if @whois_updates[user].empty? && !user.attr(:unknown?, true, true)
|
189
|
+
# for some reason, we did not receive user information. one
|
190
|
+
# reason is freenode throttling WHOIS
|
191
|
+
Thread.new do
|
192
|
+
sleep 2
|
193
|
+
user.whois
|
194
|
+
end
|
195
|
+
else
|
196
|
+
{
|
197
|
+
:authname => nil,
|
198
|
+
:idle => 0,
|
199
|
+
:secure? => false,
|
200
|
+
}.merge(@whois_updates[user]).each do |attr, value|
|
201
|
+
user.sync(attr, value, true)
|
202
|
+
end
|
203
|
+
|
204
|
+
user.sync(:unknown?, false, true)
|
205
|
+
user.synced = true
|
206
|
+
@whois_updates.delete user
|
207
|
+
end
|
208
|
+
elsif msg.command == RPL_ENDOFNAMES.to_s
|
209
|
+
@in_lists.delete :names
|
210
|
+
msg.channel.mark_as_synced(:users)
|
211
|
+
elsif msg.command == RPL_BANLIST.to_s
|
212
|
+
unless @in_lists.include?(:bans)
|
213
|
+
msg.channel.bans_unsynced.clear
|
214
|
+
end
|
215
|
+
@in_lists << :bans
|
216
|
+
|
217
|
+
mask = msg.params[2]
|
218
|
+
by = User.find_ensured(msg.params[3].split("!").first, @bot)
|
219
|
+
at = Time.at(msg.params[4].to_i)
|
220
|
+
|
221
|
+
ban = Ban.new(mask, by, at)
|
222
|
+
msg.channel.bans_unsynced << ban
|
223
|
+
elsif msg.command == RPL_ENDOFBANLIST.to_s
|
224
|
+
if @in_lists.include?(:bans)
|
225
|
+
@in_lists.delete :bans
|
226
|
+
else
|
227
|
+
# we never received a ban, yet an end of list => no bans
|
228
|
+
msg.channel.bans_unsynced.clear
|
229
|
+
end
|
230
|
+
|
231
|
+
msg.channel.mark_as_synced(:bans)
|
232
|
+
end
|
233
|
+
|
234
|
+
# TODO work with strings/constants, too
|
235
|
+
|
236
|
+
if msg.error?
|
237
|
+
events << :error
|
238
|
+
else
|
239
|
+
events << msg.command.downcase.to_sym
|
240
|
+
end
|
241
|
+
|
242
|
+
dispatch_msg = msg
|
243
|
+
end
|
244
|
+
|
245
|
+
events.each do |event|
|
246
|
+
@bot.dispatch(event, dispatch_msg)
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
# @return [Boolean] true if we successfully registered yet
|
251
|
+
def registered?
|
252
|
+
(("001".."004").to_a - @registration).empty?
|
253
|
+
end
|
254
|
+
|
255
|
+
# Send a message.
|
256
|
+
# @return [void]
|
257
|
+
def message(msg)
|
258
|
+
@queue.queue(msg)
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
module Newton
|
2
|
+
class ISupport < Hash
|
3
|
+
@@mappings = {
|
4
|
+
%w[PREFIX] => lambda {|v|
|
5
|
+
modes, prefixes = v.match(/^\((.+)\)(.+)$/)[1..2]
|
6
|
+
h = {}
|
7
|
+
modes.split("").each_with_index do |c, i|
|
8
|
+
h[c] = prefixes[i]
|
9
|
+
end
|
10
|
+
h
|
11
|
+
},
|
12
|
+
|
13
|
+
%w[CHANTYPES] => lambda {|v| v.split("")},
|
14
|
+
%w[CHANMODES] => lambda {|v|
|
15
|
+
h = {}
|
16
|
+
h["A"], h["B"], h["C"], h["D"] = v.split(",").map {|l| l.split("")}
|
17
|
+
h
|
18
|
+
},
|
19
|
+
|
20
|
+
%w[MODES MAXCHANNELS NICKLEN MAXBANS TOPICLEN
|
21
|
+
KICKLEN CHANNELLEN CHIDLEN SILENCE AWAYLEN
|
22
|
+
MAXTARGETS WATCH] => lambda {|v| v.to_i},
|
23
|
+
|
24
|
+
%w[CHANLIMIT MAXLIST IDCHAN] => lambda {|v|
|
25
|
+
h = {}
|
26
|
+
v.split(",").each do |pair|
|
27
|
+
args, num = pair.split(":")
|
28
|
+
args.split("").each do |arg|
|
29
|
+
h[arg] = num.to_i
|
30
|
+
end
|
31
|
+
end
|
32
|
+
h
|
33
|
+
},
|
34
|
+
|
35
|
+
%w[TARGMAX] => lambda {|v|
|
36
|
+
h = {}
|
37
|
+
v.split(",").each do |pair|
|
38
|
+
name, value = pair.split(":")
|
39
|
+
h[name] = value.to_i
|
40
|
+
end
|
41
|
+
h
|
42
|
+
},
|
43
|
+
|
44
|
+
%w[NETWORK] => lambda {|v| v},
|
45
|
+
%w[STATUSMSG] => lambda {|v| v.split("")},
|
46
|
+
%w[CASEMAPPING] => lambda {|v| v.to_sym},
|
47
|
+
%w[ELIST] => lambda {|v| v.split("")},
|
48
|
+
# TODO STD
|
49
|
+
}
|
50
|
+
|
51
|
+
def initialize(*args)
|
52
|
+
super
|
53
|
+
# by setting most numeric values to "Infinity", we let the
|
54
|
+
# server truncate messages and lists while at the same time
|
55
|
+
# allowing the use of strictness=:strict for servers that don't
|
56
|
+
# support ISUPPORT (hopefully none, anyway)
|
57
|
+
|
58
|
+
self["PREFIX"] = {"o" => "@", "v" => "+"}
|
59
|
+
self["CHANTYPES"] = ["#"]
|
60
|
+
self["CHANMODES"] = {
|
61
|
+
"A" => ["b"],
|
62
|
+
"B" => ["k"],
|
63
|
+
"C" => ["l"],
|
64
|
+
"D" => %w[i m n p s t r]
|
65
|
+
}
|
66
|
+
self["MODES"] = 1
|
67
|
+
self["NICKLEN"] = Infinity
|
68
|
+
self["MAXBANS"] = Infinity
|
69
|
+
self["TOPICLEN"] = Infinity
|
70
|
+
self["KICKLEN"] = Infinity
|
71
|
+
self["CHANNELLEN"] = Infinity
|
72
|
+
self["CHIDLEN"] = 5
|
73
|
+
self["AWAYLEN"] = Infinity
|
74
|
+
self["MAXTARGETS"] = 1
|
75
|
+
self["MAXCHANNELS"] = Infinity # deprecated
|
76
|
+
self["CHANLIMIT"] = {"#" => Infinity}
|
77
|
+
self["STATUSMSG"] = ["@", "+"]
|
78
|
+
self["CASEMAPPING"] = :rfc1459
|
79
|
+
self["ELIST"] = []
|
80
|
+
end
|
81
|
+
|
82
|
+
# @api private
|
83
|
+
# @return [void]
|
84
|
+
def parse(*options)
|
85
|
+
options.each do |option|
|
86
|
+
name, value = option.split("=")
|
87
|
+
if value
|
88
|
+
proc = @@mappings.find {|key, value| key.include?(name)}
|
89
|
+
self[name] = (proc && proc[1].call(value)) || value
|
90
|
+
else
|
91
|
+
self[name] = true
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
data/lib/newton/mask.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
module Newton
|
2
|
+
class Mask
|
3
|
+
# @return [String]
|
4
|
+
attr_reader :nick
|
5
|
+
# @return [String]
|
6
|
+
attr_reader :user
|
7
|
+
# @return [String]
|
8
|
+
attr_reader :host
|
9
|
+
# @return [String]
|
10
|
+
attr_reader :mask
|
11
|
+
def initialize(mask)
|
12
|
+
@mask = mask
|
13
|
+
@nick, @user, @host = mask.match(/(.+)!(.+)@(.+)/)[1..1]
|
14
|
+
@regexp = Regexp.new(Regexp.escape(mask).gsub("\\*", ".*"))
|
15
|
+
end
|
16
|
+
|
17
|
+
# @return [Boolean]
|
18
|
+
def match(user)
|
19
|
+
mask = "%s!%s@%s" % [nick, user, host]
|
20
|
+
return mask =~ @regexp
|
21
|
+
|
22
|
+
# TODO support CIDR (freenode)
|
23
|
+
end
|
24
|
+
alias_method :=~, :match
|
25
|
+
|
26
|
+
# @return [String]
|
27
|
+
def to_s
|
28
|
+
@mask.dup
|
29
|
+
end
|
30
|
+
|
31
|
+
# @param [Ban, Mask, User, String]
|
32
|
+
# @return [Mask]
|
33
|
+
def self.from(target)
|
34
|
+
case target
|
35
|
+
when User, Ban
|
36
|
+
target.mask
|
37
|
+
when String
|
38
|
+
Mask.new(target)
|
39
|
+
when Mask
|
40
|
+
target
|
41
|
+
else
|
42
|
+
raise ArgumentError
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,162 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
module Newton
|
3
|
+
class Message
|
4
|
+
# @return [String]
|
5
|
+
attr_accessor :raw
|
6
|
+
# @return [String]
|
7
|
+
attr_accessor :prefix
|
8
|
+
# @return [String]
|
9
|
+
attr_accessor :command
|
10
|
+
# @return [Array<String>]
|
11
|
+
attr_accessor :params
|
12
|
+
|
13
|
+
|
14
|
+
def initialize(msg, bot)
|
15
|
+
@raw = msg
|
16
|
+
@bot = bot
|
17
|
+
@matches = {:ctcp => {}, :other => {}}
|
18
|
+
parse if msg
|
19
|
+
end
|
20
|
+
|
21
|
+
# @return [Boolean] true if the message is an numeric reply (as
|
22
|
+
# opposed to a command)
|
23
|
+
def numeric_reply?
|
24
|
+
!!(@numeric_reply ||= @command.match(/^\d{3}$/))
|
25
|
+
end
|
26
|
+
|
27
|
+
# @api private
|
28
|
+
# @return [void]
|
29
|
+
def parse
|
30
|
+
match = @raw.match(/(^:(\S+) )?(\S+)(.*)/)
|
31
|
+
_, @prefix, @command, raw_params = match.captures
|
32
|
+
|
33
|
+
raw_params.strip!
|
34
|
+
if match = raw_params.match(/(?:^:| :)(.*)$/)
|
35
|
+
@params = match.pre_match.split(" ")
|
36
|
+
@params << match[1]
|
37
|
+
else
|
38
|
+
@params = raw_params.split(" ")
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# @return [User] The user who sent this message
|
43
|
+
def user
|
44
|
+
return unless @prefix
|
45
|
+
nick = @prefix[/^(\S+)!/, 1]
|
46
|
+
user = @prefix[/^\S+!(\S+)@/, 1]
|
47
|
+
host = @prefix[/@(\S+)$/, 1]
|
48
|
+
|
49
|
+
return nil if nick.nil?
|
50
|
+
@user ||= User.find_ensured(user, nick, host, @bot)
|
51
|
+
end
|
52
|
+
|
53
|
+
# @return [String, nil]
|
54
|
+
def server
|
55
|
+
return unless @prefix
|
56
|
+
return if @prefix.match(/[@!]/)
|
57
|
+
@server ||= @prefix[/^(\S+)/, 1]
|
58
|
+
end
|
59
|
+
|
60
|
+
# @return [Boolean] true if the message describes an error
|
61
|
+
def error?
|
62
|
+
!!error
|
63
|
+
end
|
64
|
+
|
65
|
+
# @return [Number, nil] the numeric error code, if any
|
66
|
+
def error
|
67
|
+
@error ||= (command.to_i if numeric_reply? && command[/[45]\d\d/])
|
68
|
+
end
|
69
|
+
|
70
|
+
# @return [Boolean] true if this message was sent in a channel
|
71
|
+
def channel?
|
72
|
+
!!channel
|
73
|
+
end
|
74
|
+
|
75
|
+
# @return [Boolean] true if the message is an CTCP message
|
76
|
+
def ctcp?
|
77
|
+
params.last =~ /\001.+\001/
|
78
|
+
end
|
79
|
+
|
80
|
+
# @return [String, nil] the command part of an CTCP message
|
81
|
+
def ctcp_command
|
82
|
+
return unless ctcp?
|
83
|
+
ctcp_message.split(" ").first
|
84
|
+
end
|
85
|
+
|
86
|
+
# @return [Channel] The channel in which this message was sent
|
87
|
+
def channel
|
88
|
+
@channel ||= begin
|
89
|
+
case command
|
90
|
+
when "INVITE", RPL_CHANNELMODEIS.to_s, RPL_BANLIST.to_s
|
91
|
+
Channel.find_ensured(params[1], @bot)
|
92
|
+
when RPL_NAMEREPLY.to_s
|
93
|
+
Channel.find_ensured(params[2], @bot)
|
94
|
+
else
|
95
|
+
if params.first.start_with?("#")
|
96
|
+
Channel.find_ensured(params.first, @bot)
|
97
|
+
elsif numeric_reply? and params[1].start_with?("#")
|
98
|
+
Channel.find_ensured(params[1], @bot)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# @api private
|
105
|
+
# @return [MatchData]
|
106
|
+
def match(regexp, type)
|
107
|
+
if type == :ctcp
|
108
|
+
@matches[:ctcp][regexp] ||= ctcp_message.match(regexp)
|
109
|
+
else
|
110
|
+
@matches[:other][regexp] ||= message.match(regexp)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# @return [String, nil] the CTCP message, without \001 control characters
|
115
|
+
def ctcp_message
|
116
|
+
return unless ctcp?
|
117
|
+
params.last =~ /\001(.+)\001/
|
118
|
+
$1
|
119
|
+
end
|
120
|
+
|
121
|
+
# @return [String, nil]
|
122
|
+
def message
|
123
|
+
@message ||= begin
|
124
|
+
if error?
|
125
|
+
error.to_s
|
126
|
+
elsif regular_command?
|
127
|
+
params.last
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# Replies to a message, automatically determining if it was a
|
133
|
+
# channel or a private message.
|
134
|
+
#
|
135
|
+
# @param [String] text the message
|
136
|
+
# @param [Boolean] prefix if prefix is true and the message was in
|
137
|
+
# a channel, the reply will be prefixed by the nickname of whoever
|
138
|
+
# send the mesage
|
139
|
+
# @return [void]
|
140
|
+
def reply(text, prefix = false)
|
141
|
+
text = text.to_s
|
142
|
+
if channel && prefix
|
143
|
+
text = text.split("\n").map {|l| "#{user.nick}: #{l}"}.join("\n")
|
144
|
+
end
|
145
|
+
|
146
|
+
(channel || user).send(text)
|
147
|
+
end
|
148
|
+
|
149
|
+
# Reply to a CTCP message
|
150
|
+
#
|
151
|
+
# @return [void]
|
152
|
+
def ctcp_reply(answer)
|
153
|
+
return unless ctcp?
|
154
|
+
@bot.raw "NOTICE #{user.nick} :\001#{ctcp_command} #{answer}\001"
|
155
|
+
end
|
156
|
+
|
157
|
+
private
|
158
|
+
def regular_command?
|
159
|
+
!numeric_reply? # a command can only be numeric or "regular"…
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
require "thread"
|
3
|
+
|
4
|
+
module Newton
|
5
|
+
# @api private
|
6
|
+
class MessageQueue
|
7
|
+
def initialize(socket, bot)
|
8
|
+
@socket = socket
|
9
|
+
@queue = Queue.new
|
10
|
+
@time_since_last_send = nil
|
11
|
+
@bot = bot
|
12
|
+
|
13
|
+
@log = []
|
14
|
+
end
|
15
|
+
|
16
|
+
# @return [void]
|
17
|
+
def queue(message)
|
18
|
+
command = message.split(" ").first
|
19
|
+
|
20
|
+
if command == "PING"
|
21
|
+
@queue.unshift(message)
|
22
|
+
else
|
23
|
+
@queue << message
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# @return [void]
|
28
|
+
def process!
|
29
|
+
while true
|
30
|
+
mps = @bot.config.messages_per_second
|
31
|
+
max_queue_size = @bot.config.server_queue_size
|
32
|
+
|
33
|
+
if @log.size > 1
|
34
|
+
time_passed = 0
|
35
|
+
|
36
|
+
@log.each_with_index do |one, index|
|
37
|
+
second = @log[index+1]
|
38
|
+
time_passed += second - one
|
39
|
+
break if index == @log.size - 2
|
40
|
+
end
|
41
|
+
|
42
|
+
messages_processed = (time_passed * mps).floor
|
43
|
+
effective_size = @log.size - messages_processed
|
44
|
+
|
45
|
+
if effective_size <= 0
|
46
|
+
@log.clear
|
47
|
+
elsif effective_size >= max_queue_size
|
48
|
+
sleep 1.0/mps
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
message = @queue.pop.to_s.chomp
|
53
|
+
|
54
|
+
@log << Time.now
|
55
|
+
FormattedLogger.log(message, :outgoing) if @bot.config.verbose
|
56
|
+
|
57
|
+
@time_since_last_send = Time.now
|
58
|
+
@socket.print message + "\r\n"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
Infinity = 1.0/0.0
|
@@ -0,0 +1,18 @@
|
|
1
|
+
class Module
|
2
|
+
# @api private
|
3
|
+
def synced_attr_reader(attribute)
|
4
|
+
define_method(attribute) do
|
5
|
+
attr(attribute)
|
6
|
+
end
|
7
|
+
|
8
|
+
define_method("#{attribute}_unsynced") do
|
9
|
+
attr(attribute, false, true)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
# @api private
|
14
|
+
def synced_attr_accessor(attr)
|
15
|
+
synced_attr_reader(attr)
|
16
|
+
attr_accessor(attr)
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require "thread"
|
2
|
+
class Queue
|
3
|
+
def unshift(obj)
|
4
|
+
Thread.critical = true
|
5
|
+
@que.unshift obj
|
6
|
+
begin
|
7
|
+
t = @waiting.shift
|
8
|
+
t.wakeup if t
|
9
|
+
rescue ThreadError
|
10
|
+
retry
|
11
|
+
ensure
|
12
|
+
Thread.critical = false
|
13
|
+
end
|
14
|
+
begin
|
15
|
+
t.run if t
|
16
|
+
rescue ThreadError
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
class String
|
2
|
+
def irc_downcase(mapping)
|
3
|
+
case mapping
|
4
|
+
when :rfc1459
|
5
|
+
self.tr("A-Z[]\\^", "a-z{}|~")
|
6
|
+
when :"strict-rfc1459"
|
7
|
+
self.tr("A-Z[]\\", "a-z{}|")
|
8
|
+
else
|
9
|
+
# when :ascii or unknown/nil
|
10
|
+
self.tr("A-Z", "a-z")
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def irc_upcase(mapping)
|
15
|
+
case mapping
|
16
|
+
when :ascii
|
17
|
+
self.tr("a-z", "A-Z")
|
18
|
+
when :rfc1459
|
19
|
+
self.tr("a-z{}|~", "A-Z[]\\^")
|
20
|
+
when :strict-rfc1459
|
21
|
+
self.tr("a-z{}|", "A-Z[]\\")
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|