net-irc2 0.0.10

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,68 @@
1
+ # coding: ASCII-8BIT
2
+ module Net::IRC::PATTERN # :nodoc:
3
+ # letter = %x41-5A / %x61-7A ; A-Z / a-z
4
+ # digit = %x30-39 ; 0-9
5
+ # hexdigit = digit / "A" / "B" / "C" / "D" / "E" / "F"
6
+ # special = %x5B-60 / %x7B-7D
7
+ # ; "[", "]", "\", "`", "_", "^", "{", "|", "}"
8
+ LETTER = 'A-Za-z'
9
+ DIGIT = '0-9'
10
+ HEXDIGIT = "#{DIGIT}A-Fa-f"
11
+ SPECIAL = '\x5B-\x60\x7B-\x7D'
12
+
13
+ # shortname = ( letter / digit ) *( letter / digit / "-" )
14
+ # *( letter / digit )
15
+ # ; as specified in RFC 1123 [HNAME]
16
+ # hostname = shortname *( "." shortname )
17
+ SHORTNAME = "[#{LETTER}#{DIGIT}](?:[-#{LETTER}#{DIGIT}]*[#{LETTER}#{DIGIT}])?"
18
+ HOSTNAME = "#{SHORTNAME}(?:\\.#{SHORTNAME})*"
19
+
20
+ # servername = hostname
21
+ SERVERNAME = HOSTNAME
22
+
23
+ # nickname = ( letter / special ) *8( letter / digit / special / "-" )
24
+ #NICKNAME = "[#{LETTER}#{SPECIAL}#{DIGIT}_][-#{LETTER}#{DIGIT}#{SPECIAL}]*"
25
+ NICKNAME = "\\S+" # for multibytes
26
+
27
+ # user = 1*( %x01-09 / %x0B-0C / %x0E-1F / %x21-3F / %x41-FF )
28
+ # ; any octet except NUL, CR, LF, " " and "@"
29
+ USER = '[\x01-\x09\x0B-\x0C\x0E-\x1F\x21-\x3F\x41-\xFF]+'
30
+
31
+ # ip4addr = 1*3digit "." 1*3digit "." 1*3digit "." 1*3digit
32
+ IP4ADDR = "[#{DIGIT}]{1,3}(?:\\.[#{DIGIT}]{1,3}){3}"
33
+ # ip6addr = 1*hexdigit 7( ":" 1*hexdigit )
34
+ # ip6addr =/ "0:0:0:0:0:" ( "0" / "FFFF" ) ":" ip4addr
35
+ IP6ADDR = "(?:[#{HEXDIGIT}]+(?::[#{HEXDIGIT}]+){7}|0:0:0:0:0:(?:0|FFFF):#{IP4ADDR})"
36
+ # hostaddr = ip4addr / ip6addr
37
+ HOSTADDR = "(?:#{IP4ADDR}|#{IP6ADDR})"
38
+
39
+ # host = hostname / hostaddr
40
+ HOST = "(?:#{HOSTNAME}|#{HOSTADDR})"
41
+
42
+ # prefix = servername / ( nickname [ [ "!" user ] "@" host ] )
43
+ PREFIX = "(?:#{NICKNAME}(?:(?:!#{USER})?@#{HOST})?|#{SERVERNAME})"
44
+
45
+ # nospcrlfcl = %x01-09 / %x0B-0C / %x0E-1F / %x21-39 / %x3B-FF
46
+ # ; any octet except NUL, CR, LF, " " and ":"
47
+ NOSPCRLFCL = '\x01-\x09\x0B-\x0C\x0E-\x1F\x21-\x39\x3B-\xFF'
48
+
49
+ # command = 1*letter / 3digit
50
+ COMMAND = "(?:[#{LETTER}]+|[#{DIGIT}]{3})"
51
+
52
+ # SPACE = %x20 ; space character
53
+ # middle = nospcrlfcl *( ":" / nospcrlfcl )
54
+ # trailing = *( ":" / " " / nospcrlfcl )
55
+ # params = *14( SPACE middle ) [ SPACE ":" trailing ]
56
+ # =/ 14( SPACE middle ) [ SPACE [ ":" ] trailing ]
57
+ MIDDLE = "[#{NOSPCRLFCL}][:#{NOSPCRLFCL}]*"
58
+ TRAILING = "[: #{NOSPCRLFCL}]*"
59
+ PARAMS = "(?:((?: #{MIDDLE}){0,14})(?: :(#{TRAILING}))?|((?: #{MIDDLE}){14}):?(#{TRAILING}))"
60
+
61
+ # crlf = %x0D %x0A ; "carriage return" "linefeed"
62
+ # message = [ ":" prefix SPACE ] command [ params ] crlf
63
+ CRLF = '\x0D\x0A'
64
+ MESSAGE = "(?::(#{PREFIX}) )?(#{COMMAND})#{PARAMS}\s*#{CRLF}"
65
+
66
+ CLIENT_PATTERN = /\A#{NICKNAME}(?:(?:!#{USER})?@#{HOST})\z/on
67
+ MESSAGE_PATTERN = /\A#{MESSAGE}\z/on
68
+ end # PATTERN
@@ -0,0 +1,186 @@
1
+ class Net::IRC::Server
2
+ # Server global state for accessing Server::Session
3
+ attr_accessor :state
4
+ attr_accessor :sessions
5
+
6
+ def initialize(host, port, session_class, opts={})
7
+ @host = host
8
+ @port = port
9
+ @session_class = session_class
10
+ @opts = OpenStruct.new(opts)
11
+ @sessions = []
12
+ @state = {}
13
+ end
14
+
15
+ # Start server loop.
16
+ def start
17
+ @serv = TCPServer.new(@host, @port)
18
+ @log = @opts.logger || Logger.new($stdout)
19
+ @log.info "Host: #{@host} Port:#{@port}"
20
+ @accept = Thread.start do
21
+ loop do
22
+ Thread.start(@serv.accept) do |s|
23
+ begin
24
+ @log.info "Client connected, new session starting..."
25
+ s = @session_class.new(self, s, @log, @opts)
26
+ @sessions << s
27
+ s.start
28
+ rescue Exception => e
29
+ puts e
30
+ puts e.backtrace
31
+ ensure
32
+ @sessions.delete(s)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ @accept.join
38
+ end
39
+
40
+ # Close all sessions.
41
+ def finish
42
+ Thread.exclusive do
43
+ @accept.kill
44
+ begin
45
+ @serv.close
46
+ rescue
47
+ end
48
+ @sessions.each do |s|
49
+ s.finish
50
+ end
51
+ end
52
+ end
53
+
54
+
55
+ class Session
56
+ include Net::IRC
57
+ include Constants
58
+
59
+ attr_reader :prefix, :nick, :real, :host
60
+
61
+ # Override subclass.
62
+ def server_name
63
+ "net-irc"
64
+ end
65
+
66
+ # Override subclass.
67
+ def server_version
68
+ "0.0.0"
69
+ end
70
+
71
+ # Override subclass.
72
+ def available_user_modes
73
+ "eixwy"
74
+ end
75
+
76
+ # Override subclass.
77
+ def available_channel_modes
78
+ "spknm"
79
+ end
80
+
81
+ def initialize(server, socket, logger, opts={})
82
+ @server, @socket, @log, @opts = server, socket, logger, opts
83
+ end
84
+
85
+ def self.start(*args)
86
+ new(*args).start
87
+ end
88
+
89
+ # Start session loop.
90
+ def start
91
+ on_connected
92
+ while l = @socket.gets
93
+ begin
94
+ @log.debug "RECEIVE: #{l.chomp}"
95
+ m = Message.parse(l)
96
+ next if on_message(m) === true
97
+
98
+ name = "on_#{(COMMANDS[m.command.upcase] || m.command).downcase}"
99
+ send(name, m) if respond_to?(name)
100
+
101
+ break if m.command == QUIT
102
+ rescue Message::InvalidMessage
103
+ @log.error "MessageParse: " + l.inspect
104
+ end
105
+ end
106
+ rescue IOError
107
+ ensure
108
+ finish
109
+ end
110
+
111
+ # Close this session.
112
+ def finish
113
+ begin
114
+ @socket.close
115
+ rescue
116
+ end
117
+ on_disconnected
118
+ end
119
+
120
+ # Default PASS callback.
121
+ # Set @pass.
122
+ def on_pass(m)
123
+ @pass = m.params[0]
124
+ end
125
+
126
+ # Default NICK callback.
127
+ # Set @nick.
128
+ def on_nick(m)
129
+ @nick = m.params[0]
130
+ @prefix = Prefix.new("#{@nick}!#{@user}@#{@host}") if defined? @prefix
131
+ end
132
+
133
+ # Default USER callback.
134
+ # Set @user, @real, @host and call initial_message.
135
+ def on_user(m)
136
+ @user, @real = m.params[0], m.params[3]
137
+ @nick ||= @user
138
+ @host = @socket.peeraddr[2]
139
+ @prefix = Prefix.new("#{@nick}!#{@user}@#{@host}")
140
+ initial_message
141
+ end
142
+
143
+ # Call when socket connected.
144
+ def on_connected
145
+ end
146
+
147
+ # Call when socket closed.
148
+ def on_disconnected
149
+ end
150
+
151
+ # Catch all messages.
152
+ # If this method return true, aother callback will not be called.
153
+ def on_message(m)
154
+ end
155
+
156
+ # Default PING callback. Response PONG.
157
+ def on_ping(m)
158
+ post server_name, PONG, m.params[0]
159
+ end
160
+
161
+ private
162
+ # Post message to server.
163
+ #
164
+ # include Net::IRC::Constants
165
+ # post prefix, PRIVMSG, "#channel", "foobar"
166
+ def post(prefix, command, *params)
167
+ m = Message.new(prefix, command, params.map {|s|
168
+ #s.gsub(/\r\n|[\r\n]/, " ")
169
+ s.tr("\r\n", " ")
170
+ })
171
+ @log.debug "SEND: #{m.to_s.chomp}"
172
+ @socket << m
173
+ rescue IOError
174
+ finish
175
+ end
176
+
177
+ # Call when client connected.
178
+ # Send RPL_WELCOME sequence. If you want to customize, override this method at subclass.
179
+ def initial_message
180
+ post server_name, RPL_WELCOME, @nick, "Welcome to the Internet Relay Network #{@prefix}"
181
+ post server_name, RPL_YOURHOST, @nick, "Your host is #{server_name}, running version #{server_version}"
182
+ post server_name, RPL_CREATED, @nick, "This server was created #{Time.now}"
183
+ post server_name, RPL_MYINFO, @nick, "#{server_name} #{server_version} #{available_user_modes} #{available_channel_modes}"
184
+ end
185
+ end
186
+ end # Server
data/lib/net/irc.rb ADDED
@@ -0,0 +1,77 @@
1
+ #!ruby
2
+
3
+ require "bundler"
4
+ Bundler.setup
5
+
6
+ require "ostruct"
7
+ require "socket"
8
+ require "logger"
9
+ require 'buftok'
10
+
11
+ module Net; end
12
+
13
+ module Net::IRC
14
+ VERSION = "0.0.10".freeze
15
+ class IRCException < StandardError; end
16
+
17
+ require "net/irc/constants"
18
+ require "net/irc/pattern"
19
+
20
+ autoload :Message, "net/irc/message"
21
+ autoload :Client, "net/irc/client"
22
+ autoload :Server, "net/irc/server"
23
+
24
+ class Prefix < String
25
+ def nick
26
+ extract[0]
27
+ end
28
+
29
+ def user
30
+ extract[1]
31
+ end
32
+
33
+ def host
34
+ extract[2]
35
+ end
36
+
37
+ # Extract Prefix String to [nick, user, host] Array.
38
+ def extract
39
+ _, *ret = *self.match(/\A([^\s!]+)(?:!([^\s@]+)@(\S+))?\z/)
40
+ ret
41
+ end
42
+ end
43
+
44
+ # Encode to CTCP message. Prefix and postfix \x01.
45
+ def ctcp_encode(str)
46
+ "\x01#{ctcp_quote(str)}\x01"
47
+ end
48
+ #alias :ctcp_encoding :ctcp_encode
49
+ module_function :ctcp_encode #, :ctcp_encoding
50
+
51
+ # Decode from CTCP message delimited with \x01.
52
+ def ctcp_decode(str)
53
+ ctcp_dequote(str.delete("\x01"))
54
+ end
55
+ #alias :ctcp_decoding :ctcp_decode
56
+ module_function :ctcp_decode #, :ctcp_decoding
57
+
58
+ def ctcp_quote(str)
59
+ low_quote(str.gsub("\\", "\\\\\\\\").gsub("\x01", "\\a"))
60
+ end
61
+ module_function :ctcp_quote
62
+
63
+ def ctcp_dequote(str)
64
+ low_dequote(str).gsub("\\a", "\x01").gsub(/\\(.|\z)/m, "\\1")
65
+ end
66
+ module_function :ctcp_dequote
67
+
68
+ private
69
+ def low_quote(str)
70
+ str.gsub("\x10", "\x10\x10").gsub("\x00", "\x10\x30").gsub("\r", "\x10r").gsub("\n", "\x10n")
71
+ end
72
+
73
+ def low_dequote(str)
74
+ str.gsub("\x10n", "\n").gsub("\x10r", "\r").gsub("\x10\x30", "\x00").gsub("\x10\x10", "\x10")
75
+ end
76
+ end
77
+
@@ -0,0 +1,184 @@
1
+ #!spec
2
+
3
+ $LOAD_PATH << "lib"
4
+ $LOAD_PATH << "../lib"
5
+
6
+ require "rubygems"
7
+ require "rspec"
8
+ require "thread"
9
+ require "net/irc"
10
+ require "net/irc/client/channel_manager"
11
+ include Net::IRC
12
+ include Constants
13
+
14
+ class ChannelManagerTestServerSession < Net::IRC::Server::Session
15
+ @@testq = SizedQueue.new(1)
16
+ @@instance = nil
17
+
18
+ def self.testq
19
+ @@testq
20
+ end
21
+
22
+ def self.instance
23
+ @@instance
24
+ end
25
+
26
+ def initialize(*args)
27
+ super
28
+ @@instance = self
29
+ end
30
+
31
+ def on_message(m)
32
+ @@testq << m
33
+ end
34
+ end
35
+
36
+ class ChannelManagerTestClient < Net::IRC::Client
37
+ include Net::IRC::Client::ChannelManager
38
+ @@testq = SizedQueue.new(1)
39
+
40
+ def self.testq
41
+ @@testq
42
+ end
43
+
44
+ def on_message(m)
45
+ @@testq << m
46
+ end
47
+ end
48
+
49
+ describe Net::IRC, "server and client" do
50
+ before :all do
51
+ @port = nil
52
+ @server, @client = nil, nil
53
+
54
+ Thread.abort_on_exception = true
55
+ Thread.start do
56
+ @server = Net::IRC::Server.new("localhost", @port, ChannelManagerTestServerSession, {
57
+ :logger => Logger.new(nil),
58
+ })
59
+ @server.start
60
+ end
61
+
62
+ Thread.pass
63
+ true until @server.instance_variable_get(:@serv)
64
+
65
+ @port = @server.instance_variable_get(:@serv).addr[1]
66
+
67
+ Thread.start do
68
+ @client = ChannelManagerTestClient.new("localhost", @port, {
69
+ :nick => "foonick",
70
+ :user => "foouser",
71
+ :real => "foo real name",
72
+ :pass => "foopass",
73
+ :logger => Logger.new(nil),
74
+ })
75
+ @client.start
76
+ end
77
+
78
+ Thread.pass
79
+ true until @client
80
+ end
81
+
82
+ server_q = ChannelManagerTestServerSession.testq
83
+ client_q = ChannelManagerTestClient.testq
84
+
85
+ it "client should manage channel mode/users correctly" do
86
+ client = @client
87
+ client.instance_variable_set(:@prefix, Prefix.new("foonick!foouser@localhost"))
88
+
89
+ true until ChannelManagerTestServerSession.instance
90
+ ChannelManagerTestServerSession.instance.instance_eval do
91
+ Thread.exclusive do
92
+ post client.prefix, JOIN, "#test"
93
+ post nil, NOTICE, "#test", "sep1"
94
+ end
95
+ end
96
+
97
+ true until client_q.pop.to_s == "NOTICE #test sep1\r\n"
98
+
99
+ c = @client.instance_variable_get(:@channels)
100
+ c.synchronize do
101
+ c.should be_a_kind_of(Hash)
102
+ c["#test"].should be_a_kind_of(Hash)
103
+ c["#test"][:modes].should be_a_kind_of(Array)
104
+ c["#test"][:users].should be_a_kind_of(Array)
105
+ c["#test"][:users].should == ["foonick"]
106
+ end
107
+
108
+ ChannelManagerTestServerSession.instance.instance_eval do
109
+ Thread.exclusive do
110
+ post "test1!test@localhost", JOIN, "#test"
111
+ post "test2!test@localhost", JOIN, "#test"
112
+ post nil, NOTICE, "#test", "sep2"
113
+ end
114
+ end
115
+
116
+ true until client_q.pop.to_s == "NOTICE #test sep2\r\n"
117
+ c.synchronize do
118
+ c["#test"][:users].should == ["foonick", "test1", "test2"]
119
+ end
120
+
121
+ ChannelManagerTestServerSession.instance.instance_eval do
122
+ Thread.exclusive do
123
+ post nil, RPL_NAMREPLY, client.prefix.nick, "@", "#test", "foo1 foo2 foo3 @foo4 +foo5"
124
+ post nil, NOTICE, "#test", "sep3"
125
+ end
126
+ end
127
+
128
+ true until client_q.pop.to_s == "NOTICE #test sep3\r\n"
129
+ c.synchronize do
130
+ c["#test"][:users].should == ["foonick", "test1", "test2", "foo1", "foo2", "foo3", "foo4", "foo5"]
131
+ c["#test"][:modes].should include([:s, nil])
132
+ c["#test"][:modes].should include([:o, "foo4"])
133
+ c["#test"][:modes].should include([:v, "foo5"])
134
+ end
135
+
136
+ ChannelManagerTestServerSession.instance.instance_eval do
137
+ Thread.exclusive do
138
+ post nil, RPL_NAMREPLY, client.prefix.nick, "@", "#test1", "foo1 foo2 foo3 @foo4 +foo5"
139
+ post "foo4!foo@localhost", QUIT, "message"
140
+ post "foo5!foo@localhost", PART, "#test1", "message"
141
+ post client.prefix, KICK, "#test", "foo1", "message"
142
+ post client.prefix, MODE, "#test", "+o", "foo2"
143
+ post nil, NOTICE, "#test", "sep4"
144
+ end
145
+ end
146
+
147
+ true until client_q.pop.to_s == "NOTICE #test sep4\r\n"
148
+ c.synchronize do
149
+ c["#test"][:users].should == ["foonick", "test1", "test2", "foo2", "foo3", "foo5"]
150
+ c["#test1"][:users].should == ["foo1", "foo2", "foo3"]
151
+ c["#test"][:modes].should_not include([:o, "foo4"])
152
+ c["#test"][:modes].should include([:v, "foo5"])
153
+ c["#test1"][:modes].should_not include([:v, "foo5"])
154
+ c["#test"][:modes].should include([:o, "foo2"])
155
+ end
156
+
157
+ ChannelManagerTestServerSession.instance.instance_eval do
158
+ Thread.exclusive do
159
+ post "foonick!test@localhost", NICK, "foonick2"
160
+ post "foonick2!test@localhost", NICK, "foonick"
161
+ post "foo2!test@localhost", NICK, "bar2"
162
+ post "foo3!test@localhost", NICK, "bar3"
163
+ post nil, NOTICE, "#test", "sep5"
164
+ end
165
+ end
166
+
167
+ true until client_q.pop.to_s == "NOTICE #test sep5\r\n"
168
+ c.synchronize do
169
+ c["#test"][:users].should == ["foonick", "test1", "test2", "bar2", "bar3", "foo5"]
170
+ c["#test1"][:users].should == ["foo1", "bar2", "bar3"]
171
+ c["#test"][:modes].should_not include([:o, "foo4"])
172
+ c["#test"][:modes].should include([:v, "foo5"])
173
+ c["#test1"][:modes].should_not include([:v, "foo5"])
174
+ c["#test"][:modes].should_not include([:o, "foo2"])
175
+ c["#test"][:modes].should include([:o, "bar2"])
176
+ end
177
+ end
178
+
179
+ after :all do
180
+ @server.finish
181
+ @client.finish
182
+ end
183
+ end
184
+
@@ -0,0 +1,165 @@
1
+
2
+ $LOAD_PATH << "lib"
3
+ $LOAD_PATH << "../lib"
4
+
5
+ require "net/irc"
6
+ include Net::IRC
7
+ include Constants
8
+
9
+ describe Message::ModeParser do
10
+ it "should parse RFC2812+ correctly" do
11
+ parser = Message::ModeParser.new
12
+
13
+ parser.parse("#Finish +im")[:positive].should == [[:i, nil], [:m, nil]]
14
+ parser.parse("#Finish +o Kilroy")[:positive].should == [[:o, "Kilroy"]]
15
+ parser.parse("#Finish +v Kilroy")[:positive].should == [[:v, "Kilroy"]]
16
+ parser.parse("#Fins -s")[:negative].should == [[:s, nil]]
17
+ parser.parse("#42 +k oulu")[:positive].should == [[:k, "oulu"]]
18
+ parser.parse("#eu-opers +l 10")[:positive].should == [[:l, "10"]]
19
+ parser.parse("&oulu +b")[:positive].should == [[:b, nil]]
20
+ parser.parse("&oulu +b *!*@*")[:positive].should == [[:b, "*!*@*"]]
21
+
22
+ parser.parse("&oulu +b *!*@*.edu")[:positive].should == [[:b, "*!*@*.edu"]]
23
+ parser.parse("#oulu +e")[:positive].should == [[:e, nil]]
24
+ parser.parse("#oulu +e *!*@*.edu")[:positive].should == [[:e, "*!*@*.edu"]]
25
+ parser.parse("#oulu +I")[:positive].should == [[:I, nil]]
26
+ parser.parse("#oulu +I *!*@*.edu")[:positive].should == [[:I, "*!*@*.edu"]]
27
+ parser.parse("#oulu +R")[:positive].should == [[:R, nil]]
28
+ parser.parse("#oulu +R *!*@*.edu")[:positive].should == [[:R, "*!*@*.edu"]]
29
+
30
+ parser.parse("#foo +ooo foo bar baz").should == {
31
+ :positive => [[:o, "foo"], [:o, "bar"], [:o, "baz"]],
32
+ :negative => [],
33
+ }
34
+ parser.parse("#foo +oo-o foo bar baz").should == {
35
+ :positive => [[:o, "foo"], [:o, "bar"]],
36
+ :negative => [[:o, "baz"]],
37
+ }
38
+ parser.parse("#foo -oo+o foo bar baz").should == {
39
+ :positive => [[:o, "baz"]],
40
+ :negative => [[:o, "foo"], [:o, "bar"]],
41
+ }
42
+ parser.parse("#foo +imv foo").should == {
43
+ :positive => [[:i, nil], [:m, nil], [:v, "foo"]],
44
+ :negative => [],
45
+ }
46
+
47
+ parser.parse("#foo +lk 10 foo").should == {
48
+ :positive => [[:l, "10"], [:k, "foo"]],
49
+ :negative => [],
50
+ }
51
+ parser.parse("#foo -l+k foo").should == {
52
+ :positive => [[:k, "foo"]],
53
+ :negative => [[:l, nil]],
54
+ }
55
+ parser.parse("#foo +ao foo").should == {
56
+ :positive => [[:a, nil], [:o, "foo"]],
57
+ :negative => [],
58
+ }
59
+ end
60
+
61
+ it "should parse modes of Hyperion ircd correctly" do
62
+ parser = Message::ModeParser.new
63
+ parser.set(:CHANMODES, 'bdeIq,k,lfJD,cgijLmnPQrRstz')
64
+
65
+ parser.parse("#Finish +im")[:positive].should == [[:i, nil], [:m, nil]]
66
+ parser.parse("#Finish +o Kilroy")[:positive].should == [[:o, "Kilroy"]]
67
+ parser.parse("#Finish +v Kilroy")[:positive].should == [[:v, "Kilroy"]]
68
+ parser.parse("#Fins -s")[:negative].should == [[:s, nil]]
69
+ parser.parse("#42 +k oulu")[:positive].should == [[:k, "oulu"]]
70
+ parser.parse("#eu-opers +l 10")[:positive].should == [[:l, "10"]]
71
+ parser.parse("&oulu +b")[:positive].should == [[:b, nil]]
72
+ parser.parse("&oulu +b *!*@*")[:positive].should == [[:b, "*!*@*"]]
73
+ parser.parse("&oulu +b *!*@*.edu")[:positive].should == [[:b, "*!*@*.edu"]]
74
+
75
+ parser.parse("#oulu +e")[:positive].should == [[:e, nil]]
76
+ parser.parse("#oulu +e *!*@*.edu")[:positive].should == [[:e, "*!*@*.edu"]]
77
+ parser.parse("#oulu +I")[:positive].should == [[:I, nil]]
78
+ parser.parse("#oulu +I *!*@*.edu")[:positive].should == [[:I, "*!*@*.edu"]]
79
+
80
+ parser.parse("#foo +ooo foo bar baz").should == {
81
+ :positive => [[:o, "foo"], [:o, "bar"], [:o, "baz"]],
82
+ :negative => [],
83
+ }
84
+ parser.parse("#foo +oo-o foo bar baz").should == {
85
+ :positive => [[:o, "foo"], [:o, "bar"]],
86
+ :negative => [[:o, "baz"]],
87
+ }
88
+ parser.parse("#foo -oo+o foo bar baz").should == {
89
+ :positive => [[:o, "baz"]],
90
+ :negative => [[:o, "foo"], [:o, "bar"]],
91
+ }
92
+ parser.parse("#foo +imv foo").should == {
93
+ :positive => [[:i, nil], [:m, nil], [:v, "foo"]],
94
+ :negative => [],
95
+ }
96
+
97
+ parser.parse("#foo +lk 10 foo").should == {
98
+ :positive => [[:l, "10"], [:k, "foo"]],
99
+ :negative => [],
100
+ }
101
+ parser.parse("#foo -l+k foo").should == {
102
+ :positive => [[:k, "foo"]],
103
+ :negative => [[:l, nil]],
104
+ }
105
+ parser.parse("#foo +cv foo").should == {
106
+ :positive => [[:c, nil], [:v, "foo"]],
107
+ :negative => [],
108
+ }
109
+ end
110
+
111
+ it "should parse modes of Unreal ircd correctly" do
112
+ parser = Message::ModeParser.new
113
+ parser.set(:PREFIX, '(qaohv)~&@%+')
114
+ parser.set(:CHANMODES, 'beI,kfL,lj,psmntirRcOAQKVCuzNSMTG')
115
+
116
+ parser.parse("#Finish +im")[:positive].should == [[:i, nil], [:m, nil]]
117
+ parser.parse("#Finish +o Kilroy")[:positive].should == [[:o, "Kilroy"]]
118
+ parser.parse("#Finish +v Kilroy")[:positive].should == [[:v, "Kilroy"]]
119
+ parser.parse("#Fins -s")[:negative].should == [[:s, nil]]
120
+ parser.parse("#42 +k oulu")[:positive].should == [[:k, "oulu"]]
121
+ parser.parse("#eu-opers +l 10")[:positive].should == [[:l, "10"]]
122
+ parser.parse("&oulu +b")[:positive].should == [[:b, nil]]
123
+ parser.parse("&oulu +b *!*@*")[:positive].should == [[:b, "*!*@*"]]
124
+ parser.parse("&oulu +b *!*@*.edu")[:positive].should == [[:b, "*!*@*.edu"]]
125
+
126
+ parser.parse("#oulu +e")[:positive].should == [[:e, nil]]
127
+ parser.parse("#oulu +e *!*@*.edu")[:positive].should == [[:e, "*!*@*.edu"]]
128
+ parser.parse("#oulu +I")[:positive].should == [[:I, nil]]
129
+ parser.parse("#oulu +I *!*@*.edu")[:positive].should == [[:I, "*!*@*.edu"]]
130
+
131
+ parser.parse("#foo +ooo foo bar baz").should == {
132
+ :positive => [[:o, "foo"], [:o, "bar"], [:o, "baz"]],
133
+ :negative => [],
134
+ }
135
+ parser.parse("#foo +oo-o foo bar baz").should == {
136
+ :positive => [[:o, "foo"], [:o, "bar"]],
137
+ :negative => [[:o, "baz"]],
138
+ }
139
+ parser.parse("#foo -oo+o foo bar baz").should == {
140
+ :positive => [[:o, "baz"]],
141
+ :negative => [[:o, "foo"], [:o, "bar"]],
142
+ }
143
+ parser.parse("#foo +imv foo").should == {
144
+ :positive => [[:i, nil], [:m, nil], [:v, "foo"]],
145
+ :negative => [],
146
+ }
147
+
148
+ parser.parse("#foo +lk 10 foo").should == {
149
+ :positive => [[:l, "10"], [:k, "foo"]],
150
+ :negative => [],
151
+ }
152
+ parser.parse("#foo -l+k foo").should == {
153
+ :positive => [[:k, "foo"]],
154
+ :negative => [[:l, nil]],
155
+ }
156
+ parser.parse("#foo -q+ah foo bar baz").should == {
157
+ :positive => [[:a, "bar"], [:h, "baz"]],
158
+ :negative => [[:q, "foo"]],
159
+ }
160
+ parser.parse("#foo +Av foo").should == {
161
+ :positive => [[:A, nil], [:v, "foo"]],
162
+ :negative => [],
163
+ }
164
+ end
165
+ end