newton 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,55 @@
1
+ module Newton
2
+ module Syncable
3
+ # Blocks until the object is synced.
4
+ #
5
+ # @return [void]
6
+ def wait_until_synced(attr)
7
+ attr = attr.to_sym
8
+ while true
9
+ return if @synced_attributes.include?(attr)
10
+ sleep 0.1
11
+ end
12
+ end
13
+
14
+ # @api private
15
+ # @return [void]
16
+ def sync(attribute, value, data = false)
17
+ if data
18
+ @data[attribute] = value
19
+ else
20
+ instance_variable_set("@#{attribute}", value)
21
+ end
22
+ @synced_attributes << attribute
23
+ end
24
+
25
+ def synced?(attribute)
26
+ @synced_attributes.include?(attribute)
27
+ end
28
+
29
+ def unsync(attribute)
30
+ @synced_attributes.delete(attribute)
31
+ end
32
+
33
+ # @api private
34
+ def attr(attribute, data = false, unsync = false)
35
+ unless unsync
36
+ if @when_requesting_synced_attribute
37
+ @when_requesting_synced_attribute.call(attribute)
38
+ end
39
+ wait_until_synced(attribute)
40
+ end
41
+
42
+ if data
43
+ return @data[attribute]
44
+ else
45
+ return instance_variable_get("@#{attribute}")
46
+ end
47
+ end
48
+
49
+ # @api private
50
+ # @return [void]
51
+ def mark_as_synced(attribute)
52
+ @synced_attributes << attribute
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,226 @@
1
+ # -*- coding: utf-8 -*-
2
+ module Newton
3
+ class User
4
+ include Syncable
5
+
6
+ @users = {}
7
+ class << self
8
+
9
+ # @overload find_ensured(nick, bot)
10
+ # Finds or creates a user based on his nick.
11
+ #
12
+ # @param [String] nick The user's nickname
13
+ # @param [Bot] bot An instance of Bot
14
+ # @overload find_ensured(user, nick, host, bot)
15
+ # Finds or creates a user based on his nick but already
16
+ # setting user and host.
17
+ #
18
+ # @param [String] user The username
19
+ # @param [String] nick The nickname
20
+ # @param [String] host The user's hostname
21
+ # @param [Bot] bot An instance of bot
22
+ #
23
+ # @return [User]
24
+ def find_ensured(*args)
25
+ # FIXME CASEMAPPING
26
+ case args.size
27
+ when 2
28
+ nick = args.first
29
+ bargs = [args.first]
30
+ bot = args.last
31
+ when 4
32
+ nick = args[1]
33
+ bot = args.pop
34
+ bargs = args
35
+ else
36
+ raise ArgumentError
37
+ end
38
+ downcased_nick = nick.irc_downcase(bot.irc.isupport["CASEMAPPING"])
39
+ @users[downcased_nick] ||= new(*bargs, bot)
40
+ @users[downcased_nick]
41
+ end
42
+
43
+ # Finds a user.
44
+ #
45
+ # @param [String] nick nick of a user
46
+ # @return [User, nil]
47
+ def find(nick)
48
+ @users[nick]
49
+ end
50
+
51
+ # @return [Array<User>] Returns all users
52
+ def all
53
+ @users.values
54
+ end
55
+ end
56
+
57
+
58
+ # @return [String]
59
+ attr_accessor :nick
60
+ # @return [Bot]
61
+ attr_accessor :bot
62
+ # @return [Boolean]
63
+ attr_accessor :synced
64
+
65
+ # By default, you can use methods like User#user, User#host and
66
+ # alike – If you however fear that another thread might change
67
+ # data while you're using it and if this means a critical issue to
68
+ # your code, you can store the result of this method and work with
69
+ # that instead.
70
+ #
71
+ # @example
72
+ # on :channel do
73
+ # data = user.data
74
+ # do_something_with(data.user)
75
+ # do_something_with(data.host)
76
+ # end
77
+ # @return [Hash]
78
+ attr_accessor :data
79
+ def initialize(*args)
80
+ @data = {
81
+ :user => nil,
82
+ :host => nil,
83
+ :realname => nil,
84
+ :authname => nil,
85
+ :idle => 0,
86
+ :signed_on_at => nil,
87
+ :unknown? => false,
88
+ :channels => [],
89
+ :secure? => false,
90
+ }
91
+ case args.size
92
+ when 2
93
+ @nick, @bot = args
94
+ when 4
95
+ @data[:user], @nick, @data[:host], @bot = args
96
+ else
97
+ raise ArgumentError
98
+ end
99
+
100
+ @synced_attributes = Set.new
101
+
102
+ @when_requesting_synced_attribute = lambda {|attr|
103
+ unless @synced
104
+ unsync attr
105
+ whois
106
+ end
107
+ }
108
+ end
109
+
110
+ # Checks if the user is identified. Currently officially supports
111
+ # Quakenet and Freenode.
112
+ #
113
+ # @return [Boolean] true if the user is identified
114
+ def authed?
115
+ @data[:authname]
116
+ end
117
+
118
+ # Queries the IRC server for information on the user. This will
119
+ # set the User's state to not synced. After all information are
120
+ # received, the object will be set back to synced.
121
+ #
122
+ # @return [void]
123
+ def whois
124
+ @synced = false
125
+ @data.keys.each do |attr|
126
+ unsync attr
127
+ end
128
+ @bot.raw "WHOIS #@nick #@nick"
129
+ end
130
+
131
+ # Send a message to the user.
132
+ #
133
+ # @param [String] message the message
134
+ # @return [void]
135
+ def send(message)
136
+ @bot.msg(@nick, message)
137
+ end
138
+ alias_method :privmsg, :send
139
+
140
+ # Send a CTCP to the user.
141
+ #
142
+ # @param [String] message the ctcp message
143
+ # @return [void]
144
+ def ctcp(message)
145
+ send "\001#{message}\001"
146
+ end
147
+
148
+ # Send an action (/me) to the user.
149
+ #
150
+ # @param [String] message the message
151
+ # @return [void]
152
+ def action(message)
153
+ @bot.action(@name, message)
154
+ end
155
+
156
+ # @return [String]
157
+ def to_s
158
+ @nick
159
+ end
160
+
161
+ # @return [String]
162
+ def inspect
163
+ "#<User nick=#{@nick.inspect}>"
164
+ end
165
+
166
+ # Generates a mask for the user.
167
+ #
168
+ # @param [String] s a pattern for generating the mask. %n =
169
+ # nickname – %u = username – %h = host – %r = realname – %a =
170
+ # authname
171
+ #
172
+ # @return [Mask]
173
+ def mask(s = "%n!%u@%h")
174
+ s = s.gsub(/%(.)/) {
175
+ case $1
176
+ when "n"
177
+ @nick
178
+ when "u"
179
+ self.user
180
+ when "h"
181
+ self.host
182
+ when "r"
183
+ self.realname
184
+ when "a"
185
+ self.authname
186
+ end
187
+ }
188
+
189
+ Mask.new(s)
190
+ end
191
+
192
+ # Provides synced access to user attributes.
193
+ def method_missing(m, *args)
194
+ if m.to_s =~ /^(.+)_unsynced$/
195
+ m = $1.to_sym
196
+ unsync = true
197
+ end
198
+
199
+ if @data.has_key?(m)
200
+ attr(m, true, unsync = false)
201
+ else
202
+ super
203
+ end
204
+ end
205
+
206
+ # @return [Boolean]
207
+ def ==(other)
208
+ return case other
209
+ when self.class
210
+ @nick == other.nick
211
+ when String
212
+ self.to_s == other
213
+ when Bot
214
+ self.nick == other.config.nick
215
+ else
216
+ false
217
+ end
218
+ end
219
+ alias_method :eql?, "=="
220
+
221
+ # @return [Fixnum]
222
+ def hash
223
+ @nick.hash
224
+ end
225
+ end
226
+ end
data/lib/newton.rb ADDED
@@ -0,0 +1 @@
1
+ require 'newton/bot'
data/test/helper.rb ADDED
@@ -0,0 +1,60 @@
1
+ $LOAD_PATH.unshift 'lib'
2
+ require 'newton'
3
+ require 'rubygems'
4
+ require 'test/unit'
5
+ require 'contest'
6
+ require 'rr'
7
+ require 'timeout'
8
+ begin
9
+ require 'ruby-debug'
10
+ rescue LoadError; end
11
+
12
+ module Test::Unit::Assertions
13
+ def assert_empty_buffer(io)
14
+ assert_raise(Errno::EAGAIN) { io.read_nonblock 1 }
15
+ end
16
+ end
17
+
18
+ class MockSocket
19
+ def self.pipe
20
+ socket1, socket2 = new, new
21
+ socket1.in, socket2.out = IO.pipe
22
+ socket2.in, socket1.out = IO.pipe
23
+ [socket1, socket2]
24
+ end
25
+
26
+ attr_accessor :in, :out
27
+ def gets()
28
+ Timeout.timeout(1) {@in.gets}
29
+ end
30
+ def puts(m) @out.puts(m) end
31
+ def print(m) @out.print(m) end
32
+ def eof?() @in.eof? end
33
+ def empty?
34
+ begin
35
+ @in.read_nonblock(1)
36
+ false
37
+ rescue Errno::EAGAIN
38
+ true
39
+ end
40
+ end
41
+ end
42
+
43
+ class Test::Unit::TestCase
44
+ include RR::Adapters::TestUnit
45
+
46
+ def mock_bot(&b)
47
+ @socket, @server = MockSocket.pipe
48
+ stub(TCPSocket).open(anything, anything) {@socket}
49
+ bot = Newton::Bot.new(&b)
50
+ bot.config.environment = :test
51
+ Thread.start { bot.start }
52
+ bot
53
+ end
54
+
55
+ def bot_is_connected
56
+ assert_equal "NICK isaac\r\n", @server.gets
57
+ assert_equal "USER isaac 0 * :Isaac\r\n", @server.gets
58
+ 1.upto(4) {|i| @server.print ":localhost 00#{i}\r\n"}
59
+ end
60
+ end
@@ -0,0 +1,85 @@
1
+ require File.join(File.dirname(__FILE__), 'helper')
2
+
3
+ class TestCommands < Test::Unit::TestCase
4
+ test "raw messages can be send" do
5
+ bot = mock_bot {}
6
+ bot_is_connected
7
+
8
+ bot.raw "PRIVMSG foo :bar baz"
9
+ assert_equal "PRIVMSG foo :bar baz\r\n", @server.gets
10
+ end
11
+
12
+ test "messages are sent to recipient" do
13
+ bot = mock_bot {}
14
+ bot_is_connected
15
+
16
+ bot.msg "foo", "bar baz"
17
+ assert_equal "PRIVMSG foo :bar baz\r\n", @server.gets
18
+ end
19
+
20
+ test "actions are sent to recipient" do
21
+ bot = mock_bot {}
22
+ bot_is_connected
23
+
24
+ bot.action "foo", "bar"
25
+ assert_equal "PRIVMSG foo :\001ACTION bar\001\r\n", @server.gets
26
+ end
27
+
28
+ test "channels are joined" do
29
+ bot = mock_bot {}
30
+ bot_is_connected
31
+
32
+ bot.join "#foo", "#bar"
33
+ assert_equal "JOIN #foo\r\n", @server.gets
34
+ assert_equal "JOIN #bar\r\n", @server.gets
35
+ end
36
+
37
+ test "channels are parted" do
38
+ bot = mock_bot {}
39
+ bot_is_connected
40
+
41
+ bot.part "#foo", "#bar"
42
+ assert_equal "PART #foo\r\n", @server.gets
43
+ assert_equal "PART #bar\r\n", @server.gets
44
+ end
45
+
46
+ test "topic is set" do
47
+ bot = mock_bot {}
48
+ bot_is_connected
49
+
50
+ bot.topic "#foo", "bar baz"
51
+ assert_equal "TOPIC #foo :bar baz\r\n", @server.gets
52
+ end
53
+
54
+ test "modes can be set" do
55
+ bot = mock_bot {}
56
+ bot_is_connected
57
+
58
+ bot.mode "#foo", "+o"
59
+ assert_equal "MODE #foo +o\r\n", @server.gets
60
+ end
61
+
62
+ test "can kick users" do
63
+ bot = mock_bot {}
64
+ bot_is_connected
65
+
66
+ bot.kick "foo", "bar", "bein' a baz"
67
+ assert_equal "KICK foo bar :bein' a baz\r\n", @server.gets
68
+ end
69
+
70
+ test "quits" do
71
+ bot = mock_bot {}
72
+ bot_is_connected
73
+
74
+ bot.quit
75
+ assert_equal "QUIT\r\n", @server.gets
76
+ end
77
+
78
+ test "quits with message" do
79
+ bot = mock_bot {}
80
+ bot_is_connected
81
+
82
+ bot.quit "I'm outta here!"
83
+ assert_equal "QUIT :I'm outta here!\r\n", @server.gets
84
+ end
85
+ end
@@ -0,0 +1,89 @@
1
+ require File.join(File.dirname(__FILE__), 'helper')
2
+
3
+ class TestEvents < Test::Unit::TestCase
4
+ # This is stupid, but it's just there to make it easier to transform to the new
5
+ # Message class. Should be fixed.
6
+ def dispatch(type, env)
7
+ msg = Newton::Message.new(":john!doe@example.com PRIVMSG #foo :#{env[:message]}")
8
+ @bot.dispatch(type, msg)
9
+ end
10
+
11
+ test "events are registered" do
12
+ @bot = mock_bot {
13
+ on(:channel, /Hello/) {msg "foo", "yr formal!"}
14
+ on(:channel, /Hey/) {msg "foo", "bar baz"}
15
+ }
16
+ bot_is_connected
17
+
18
+ dispatch(:channel, :message => "Hey")
19
+ assert_equal "PRIVMSG foo :bar baz\r\n", @server.gets
20
+ end
21
+
22
+ test "catch-all events" do
23
+ @bot = mock_bot {
24
+ on(:channel) {msg "foo", "bar baz"}
25
+ }
26
+ bot_is_connected
27
+
28
+ dispatch(:channel, :message => "lolcat")
29
+ assert_equal "PRIVMSG foo :bar baz\r\n", @server.gets
30
+ end
31
+
32
+ test "event can be halted" do
33
+ @bot = mock_bot {
34
+ on(:channel, /Hey/) { halt; msg "foo", "bar baz" }
35
+ }
36
+ bot_is_connected
37
+
38
+ dispatch(:channel, :message => "Hey")
39
+ assert @server.empty?
40
+ end
41
+
42
+ test "connect-event is dispatched at connection" do
43
+ @bot = mock_bot {
44
+ on(:connect) {msg "foo", "bar baz"}
45
+ }
46
+ bot_is_connected
47
+
48
+ assert_equal "PRIVMSG foo :bar baz\r\n", @server.gets
49
+ end
50
+
51
+ test "regular expression match is accessible" do
52
+ @bot = mock_bot {
53
+ on(:channel, /foo (bar)/) {msg "foo", match[0]}
54
+ }
55
+ bot_is_connected
56
+
57
+ dispatch(:channel, :message => "foo bar")
58
+
59
+ assert_equal "PRIVMSG foo :bar\r\n", @server.gets
60
+ end
61
+
62
+ test "regular expression matches are handed to block arguments" do
63
+ @bot = mock_bot {
64
+ on :channel, /(foo) (bar)/ do |a,b|
65
+ raw "#{a}"
66
+ raw "#{b}"
67
+ end
68
+ }
69
+ bot_is_connected
70
+
71
+ dispatch(:channel, :message => "foo bar")
72
+
73
+ assert_equal "foo\r\n", @server.gets
74
+ assert_equal "bar\r\n", @server.gets
75
+ end
76
+
77
+ test "only specified number of captures are handed to block args" do
78
+ @bot = mock_bot {
79
+ on :channel, /(foo) (bar)/ do |a|
80
+ raw "#{a}"
81
+ end
82
+ }
83
+ bot_is_connected
84
+
85
+ dispatch(:channel, :message => "foo bar")
86
+
87
+ assert_equal "foo\r\n", @server.gets
88
+ end
89
+ end
@@ -0,0 +1,14 @@
1
+ require File.join(File.dirname(__FILE__), 'helper')
2
+
3
+ class TestHelpers < Test::Unit::TestCase
4
+ test "helpers are registered" do
5
+ bot = mock_bot {
6
+ helpers { def foo; msg "foo", "bar baz"; end }
7
+ on(:private, //) {foo}
8
+ }
9
+ bot_is_connected
10
+
11
+ bot.irc.parse ":johnny!john@doe.com PRIVMSG isaac :hello, you!"
12
+ assert_equal "PRIVMSG foo :bar baz\r\n", @server.gets
13
+ end
14
+ end
data/test/test_irc.rb ADDED
@@ -0,0 +1,38 @@
1
+ require File.join(File.dirname(__FILE__), 'helper')
2
+
3
+ class TestIrc < Test::Unit::TestCase
4
+ test "a new bot connects to IRC" do
5
+ bot = mock_bot {}
6
+
7
+ assert_equal "NICK isaac\r\n", @server.gets
8
+ assert_equal "USER isaac 0 * :#{bot.config.realname}\r\n", @server.gets
9
+ end
10
+
11
+ test "password is sent if specified" do
12
+ bot = mock_bot {
13
+ configure {|c| c.password = "foo"}
14
+ }
15
+ assert_equal "PASS foo\r\n", @server.gets
16
+ end
17
+
18
+ test "no messages are sent when registration isn't complete" do
19
+ bot = mock_bot {
20
+ on(:connect) {raw "Connected!"}
21
+ }
22
+ 2.times { @server.gets } # NICK / USER
23
+ bot.dispatch :connect
24
+
25
+ assert @server.empty?
26
+ end
27
+
28
+ test "no messages are sent until registration is complete" do
29
+ bot = mock_bot {
30
+ on(:connect) {raw "Connected!"}
31
+ }
32
+ 2.times { @server.gets } # NICK / USER
33
+ bot.dispatch :connect
34
+
35
+ 1.upto(4) {|i| @server.puts ":localhost 00#{i}"}
36
+ assert_equal "Connected!\r\n", @server.gets
37
+ end
38
+ end
@@ -0,0 +1,117 @@
1
+ require File.join(File.dirname(__FILE__), 'helper')
2
+
3
+ class TestMessage < Test::Unit::TestCase
4
+ include Newton
5
+
6
+ test "host prefix" do
7
+ msg = Message.new(":jeff!spicoli@beach.com QUIT")
8
+ assert_equal "jeff!spicoli@beach.com", msg.prefix
9
+ assert_equal "jeff", msg.nick
10
+ assert_equal "spicoli", msg.user
11
+ assert_equal "beach.com", msg.host
12
+ assert_nil msg.server
13
+ end
14
+
15
+ test "server prefix" do
16
+ msg = Message.new(":some.server.com PING")
17
+ assert_equal "some.server.com", msg.prefix
18
+ assert_equal "some.server.com", msg.server
19
+ assert_nil msg.nick
20
+ assert_nil msg.user
21
+ assert_nil msg.host
22
+ end
23
+
24
+ test "without prefix" do
25
+ msg = Message.new("PING foo.bar")
26
+ assert_nil msg.prefix
27
+ assert_nil msg.nick
28
+ assert_nil msg.host
29
+ end
30
+
31
+ test "command" do
32
+ msg = Message.new("PING foo.bar")
33
+ assert_equal "PING", msg.command
34
+ end
35
+
36
+ test "numeric reply" do
37
+ msg = Message.new("409")
38
+ assert msg.numeric_reply?
39
+ assert_equal "409", msg.command
40
+ end
41
+
42
+ test "single param" do
43
+ msg = Message.new("PING foo.bar")
44
+ assert_equal 1, msg.params.size
45
+ assert_equal "foo.bar", msg.params[0]
46
+ end
47
+
48
+ test "multiple params" do
49
+ msg = Message.new("FOO bar baz")
50
+ assert_equal 2, msg.params.size
51
+ assert_equal ["bar", "baz"], msg.params
52
+ end
53
+
54
+ test "single param with whitespace" do
55
+ msg = Message.new("FOO :bar baz")
56
+ assert_equal 1, msg.params.size
57
+ assert_equal "bar baz", msg.params[0]
58
+ end
59
+
60
+ test "single param with whitespace and colon" do
61
+ msg = Message.new("FOO :bar :baz")
62
+ assert_equal 1, msg.params.size
63
+ assert_equal "bar :baz", msg.params[0]
64
+ end
65
+
66
+ test "multiple params with whitespace" do
67
+ msg = Message.new("FOO bar :lol cat")
68
+ assert_equal 2, msg.params.size
69
+ assert_equal "bar", msg.params[0]
70
+ assert_equal "lol cat", msg.params[1]
71
+ end
72
+
73
+ test "multiple params with whitespace and colon" do
74
+ msg = Message.new("FOO bar :lol :cat")
75
+ assert_equal 2, msg.params.size
76
+ assert_equal "bar", msg.params[0]
77
+ assert_equal "lol :cat", msg.params[1]
78
+ end
79
+
80
+ test "error" do
81
+ msg = Message.new("200")
82
+ assert_equal false, msg.error?
83
+ assert_nil msg.error
84
+
85
+ msg = Message.new("400")
86
+ assert_equal true, msg.error?
87
+ assert_equal 400, msg.error
88
+ end
89
+
90
+ test "if error, #message has the error code string" do
91
+ msg = Message.new("400")
92
+ assert_equal "400", msg.message
93
+ end
94
+
95
+ test "channel has channel name" do
96
+ msg = Message.new(":foo!bar@baz.com PRIVMSG #awesome :lol cat")
97
+ assert_equal true, msg.channel?
98
+ assert_equal "#awesome", msg.channel
99
+ end
100
+
101
+ test "channel has nothing when receiver is a nick" do
102
+ msg = Message.new(":foo!bar@baz.com PRIVMSG john :wazzup boy?")
103
+ assert_equal false, msg.channel?
104
+ assert_equal nil, msg.channel
105
+ end
106
+
107
+ test "privmsg has #message" do
108
+ msg = Message.new(":foo!bar@baz.com PRIVMSG #awesome :lol cat")
109
+ assert_equal "lol cat", msg.message
110
+ end
111
+
112
+ # test "non-privmsg doesn't have #message" do
113
+ # msg = Message.new("PING :foo bar")
114
+ # p msg.message
115
+ # assert_nil msg.message
116
+ # end
117
+ end