newton 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -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