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