newton 0.0.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/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
|