ircbgb 0.0.1.pre
Sign up to get free protection for your applications and to get access to all the features.
- data/.autotest +19 -0
- data/.gitignore +7 -0
- data/Gemfile +20 -0
- data/Rakefile +31 -0
- data/ircbgb.gemspec +25 -0
- data/lib/ircbgb.rb +15 -0
- data/lib/ircbgb/behaviors.rb +17 -0
- data/lib/ircbgb/behaviors/negotiates_connection.rb +38 -0
- data/lib/ircbgb/behaviors/provides_commands.rb +20 -0
- data/lib/ircbgb/client.rb +103 -0
- data/lib/ircbgb/errors.rb +5 -0
- data/lib/ircbgb/matcher.rb +13 -0
- data/lib/ircbgb/message.rb +17 -0
- data/lib/ircbgb/message_parser.rb +567 -0
- data/lib/ircbgb/message_parser.rl +84 -0
- data/lib/ircbgb/server.rb +13 -0
- data/lib/ircbgb/user.rb +32 -0
- data/lib/ircbgb/version.rb +3 -0
- data/spec/ircbgb/client_spec.rb +171 -0
- data/spec/ircbgb/matcher_spec.rb +25 -0
- data/spec/ircbgb/message_parser_spec.rb +100 -0
- data/spec/ircbgb/message_spec.rb +39 -0
- data/spec/ircbgb/user_spec.rb +72 -0
- data/spec/spec_helper.rb +18 -0
- data/spec/support/dummy_io.rb +70 -0
- data/spec/support/stubz.rb +49 -0
- data/spec/support/unblocked_tcp_socket.rb +60 -0
- metadata +93 -0
@@ -0,0 +1,84 @@
|
|
1
|
+
# RFC 1459 Message Parser to be processed by Ragel
|
2
|
+
%%{
|
3
|
+
machine irc_message_parser;
|
4
|
+
|
5
|
+
action start_parser { eof = pe }
|
6
|
+
action clear_command { cmd = '' }
|
7
|
+
action clear_server { server = '' }
|
8
|
+
action clear_rest { rest = '' }
|
9
|
+
action clear_param { param = '' }
|
10
|
+
action clear_user_mask { user_mask = '' }
|
11
|
+
action append_user_mask { user_mask << data[p] }
|
12
|
+
action append_server { server << data[p] }
|
13
|
+
action append_param { param << data[p] }
|
14
|
+
action append_params { params << param }
|
15
|
+
action append_rest { rest << data[p] }
|
16
|
+
action append_command { cmd << data[p] }
|
17
|
+
action make_source_user {
|
18
|
+
source = ::Ircbgb::User.parse(user_mask)
|
19
|
+
}
|
20
|
+
action make_source_server {
|
21
|
+
source = ::Ircbgb::Server.new(server)
|
22
|
+
}
|
23
|
+
action show_me {
|
24
|
+
puts "Currently parsing data[#{p}/#{data.length}]: '#{data[p]}'"
|
25
|
+
}
|
26
|
+
action message_error {
|
27
|
+
raise ::Ircbgb::MessageFormatError, "invalid form #{data.inspect} at #{p} '#{data[p]}'"
|
28
|
+
}
|
29
|
+
|
30
|
+
sspace = ' ';
|
31
|
+
crlf = '\r\n';
|
32
|
+
whites = sspace | '\0' | '\r' | '\n';
|
33
|
+
nospcrlfcl = any - whites - ':';
|
34
|
+
nonwhite = any - whites;
|
35
|
+
userchar = any - '@' - whites;
|
36
|
+
ip4addr = digit{1,3} '.' digit{1,3} '.' digit{1,3} '.' digit{1,3};
|
37
|
+
ip6addr = (xdigit+ (':' xdigit+){7})
|
38
|
+
| ('0:0:0:0:0:' ('0' | [fF]{4}) ':' ip4addr);
|
39
|
+
hostaddr = ip4addr | ip6addr;
|
40
|
+
shortname = (alpha | digit) (alpha | digit | '-')* (alpha | digit)*;
|
41
|
+
hostname = shortname ('.' shortname)*;
|
42
|
+
host = hostname | hostaddr;
|
43
|
+
user = userchar+;
|
44
|
+
nick_special = '[' | ']' | '\\' | '`' | '_' | '^' | '{' | '|' | '}';
|
45
|
+
nick = (alpha | nick_special) (alpha | digit | nick_special | '-')*;
|
46
|
+
servername = hostname >clear_server $append_server %make_source_server;
|
47
|
+
nick_mask = (nick ('!' user ('@' host)?)?) >clear_user_mask $append_user_mask %make_source_user;
|
48
|
+
|
49
|
+
prefix = servername | nick_mask;
|
50
|
+
command = (alpha+ | digit+) >clear_command @append_command;
|
51
|
+
middle = (nospcrlfcl (':' | nospcrlfcl)*) >clear_param @append_param %append_params;
|
52
|
+
trailing = ((nospcrlfcl | ' ' | ':')*) >clear_rest @append_rest;
|
53
|
+
params = ((sspace+ middle){0,14} (sspace+ ':' trailing)?);
|
54
|
+
|
55
|
+
message = (':' prefix sspace+)? command params? crlf;
|
56
|
+
|
57
|
+
main := message >start_parser $err(message_error);
|
58
|
+
}%%
|
59
|
+
|
60
|
+
module Ircbgb
|
61
|
+
class MessageParser
|
62
|
+
|
63
|
+
%% write data;
|
64
|
+
|
65
|
+
def self.parse str
|
66
|
+
data = str
|
67
|
+
server = ''
|
68
|
+
user_mask = ''
|
69
|
+
para = ''
|
70
|
+
rest = ''
|
71
|
+
params = []
|
72
|
+
source = nil
|
73
|
+
cmd = ''
|
74
|
+
|
75
|
+
%% write init;
|
76
|
+
%% write exec;
|
77
|
+
|
78
|
+
params << rest unless rest.empty?
|
79
|
+
|
80
|
+
::Ircbgb::Message.new source, cmd, params
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
data/lib/ircbgb/user.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
module Ircbgb
|
2
|
+
class User
|
3
|
+
attr_reader :nick, :user, :host
|
4
|
+
|
5
|
+
def initialize n, u, h
|
6
|
+
@nick = n.empty? ? '*' : n
|
7
|
+
@user = u.empty? ? '*' : u
|
8
|
+
@host = h.empty? ? '*' : h
|
9
|
+
@mask = "#{@nick}!#{@user}@#{@host}"
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_s; @mask.dup; end
|
13
|
+
|
14
|
+
def =~ other
|
15
|
+
return Matcher.new(other) =~ @mask if String === other
|
16
|
+
@mask =~ other
|
17
|
+
end
|
18
|
+
|
19
|
+
class << self
|
20
|
+
def parse nick_mask
|
21
|
+
case nick_mask
|
22
|
+
when Ircbgb::User
|
23
|
+
nick_mask
|
24
|
+
when /\A([^!]+)!([^@]*)@(.*)\Z/
|
25
|
+
new $1, $2, $3
|
26
|
+
else
|
27
|
+
new nick_mask, '', ''
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,171 @@
|
|
1
|
+
require File.expand_path('../../spec_helper', __FILE__)
|
2
|
+
|
3
|
+
describe Ircbgb::Client do
|
4
|
+
include Stubz
|
5
|
+
|
6
|
+
def client
|
7
|
+
@client ||= Ircbgb::Client.new { |c|
|
8
|
+
c.servers << 'irc://server1.example.org'
|
9
|
+
c.servers << 'irc://server2.example.com:7001'
|
10
|
+
c.nicks = ['bot1', 'bot2', 'bot3']
|
11
|
+
c.realname = 'I am a bot'
|
12
|
+
c.username = 'botty'
|
13
|
+
}
|
14
|
+
end
|
15
|
+
|
16
|
+
describe "initialize" do
|
17
|
+
it "sets up servers" do
|
18
|
+
client.servers.must_equal [
|
19
|
+
'irc://server1.example.org',
|
20
|
+
'irc://server2.example.com:7001'
|
21
|
+
]
|
22
|
+
end
|
23
|
+
|
24
|
+
it "sets up nicknames" do
|
25
|
+
client.nicks.must_equal [ 'bot1', 'bot2', 'bot3' ]
|
26
|
+
end
|
27
|
+
|
28
|
+
it "sets up a realname" do
|
29
|
+
client.realname.must_equal 'I am a bot'
|
30
|
+
end
|
31
|
+
|
32
|
+
it "sets up a username" do
|
33
|
+
client.username.must_equal 'botty'
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
describe "attribs" do
|
38
|
+
it "sets servers from a single string" do
|
39
|
+
client.servers = 'irc://foo.bar.bazz'
|
40
|
+
client.servers.must_equal [ 'irc://foo.bar.bazz' ]
|
41
|
+
end
|
42
|
+
|
43
|
+
it "sets servers from an array" do
|
44
|
+
client.servers = [ 'frosty', 'snowman' ]
|
45
|
+
client.servers.must_equal [ 'frosty', 'snowman' ]
|
46
|
+
end
|
47
|
+
|
48
|
+
it "sets nicks from a single string" do
|
49
|
+
client.nicks = 'peaches'
|
50
|
+
client.nicks.must_equal [ 'peaches' ]
|
51
|
+
end
|
52
|
+
|
53
|
+
it "sets nicks from an array" do
|
54
|
+
client.nicks = ['meaty', 'meaty', 'moo']
|
55
|
+
client.nicks.must_equal ['meaty', 'meaty', 'moo']
|
56
|
+
end
|
57
|
+
|
58
|
+
it "parses server uris" do
|
59
|
+
client.uris.first.host.must_equal 'server1.example.org'
|
60
|
+
client.uris.first.port.must_equal 6667
|
61
|
+
client.uris.last.host.must_equal 'server2.example.com'
|
62
|
+
client.uris.last.port.must_equal 7001
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
describe "starting and stopping" do
|
67
|
+
before do
|
68
|
+
@mock_io = MiniTest::Mock.new
|
69
|
+
stub(::IoUnblock::TcpSocket, :new, @mock_io)
|
70
|
+
end
|
71
|
+
|
72
|
+
it "starts and stops an unblocked tcp socket and waits until its running" do
|
73
|
+
@mock_io.expect(:start, nil)
|
74
|
+
@mock_io.expect(:running?, true)
|
75
|
+
@mock_io.expect(:stop, nil)
|
76
|
+
client.start
|
77
|
+
client.stop
|
78
|
+
@mock_io.verify
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
describe "io" do
|
83
|
+
before do
|
84
|
+
@socket = socket = UnblockedTcpSocket.new
|
85
|
+
stub(::IoUnblock::TcpSocket, :new) { |*a|
|
86
|
+
socket.init_with(*a)
|
87
|
+
}
|
88
|
+
client.start
|
89
|
+
end
|
90
|
+
|
91
|
+
after do
|
92
|
+
client.stop
|
93
|
+
end
|
94
|
+
|
95
|
+
describe "connecting" do
|
96
|
+
it "is connected once it's sent the user nick and pong" do
|
97
|
+
client.connected?.must_equal false
|
98
|
+
@socket.trigger_start
|
99
|
+
@socket.server_write 'PING :give tHi:S baCK!'
|
100
|
+
client.connected?.must_equal true
|
101
|
+
@socket.written.must_equal [
|
102
|
+
'USER botty 0 * :I am a bot',
|
103
|
+
'NICK bot1',
|
104
|
+
'PONG :give tHi:S baCK!'
|
105
|
+
]
|
106
|
+
end
|
107
|
+
|
108
|
+
it "chooses alternate nicknames" do
|
109
|
+
@socket.trigger_start
|
110
|
+
@socket.server_write '433 * bot1 :Nickname is already in use.'
|
111
|
+
client.connected?.must_equal false
|
112
|
+
@socket.server_write '433 * bot2 :Nickname is already in use.'
|
113
|
+
client.connected?.must_equal false
|
114
|
+
@socket.server_write 'PING :0123456'
|
115
|
+
client.connected?.must_equal true
|
116
|
+
@socket.written.must_equal [
|
117
|
+
'USER botty 0 * :I am a bot',
|
118
|
+
'NICK bot1',
|
119
|
+
'NICK bot2',
|
120
|
+
'NICK bot3',
|
121
|
+
'PONG :0123456'
|
122
|
+
]
|
123
|
+
end
|
124
|
+
|
125
|
+
it "shuts down if all nicknames are taken" do
|
126
|
+
@socket.trigger_start
|
127
|
+
@socket.server_write '433 * bot1 :Nickname is already in use.'
|
128
|
+
@socket.server_write '433 * bot2 :Nickname is already in use.'
|
129
|
+
@socket.server_write '433 * bot3 :Nickname is already in use.'
|
130
|
+
client.connected?.must_equal false
|
131
|
+
@socket.written.must_equal [
|
132
|
+
'USER botty 0 * :I am a bot',
|
133
|
+
'NICK bot1',
|
134
|
+
'NICK bot2',
|
135
|
+
'NICK bot3',
|
136
|
+
'QUIT'
|
137
|
+
]
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
describe "events" do
|
142
|
+
it "binds and triggers an event based upon the command" do
|
143
|
+
params = nil
|
144
|
+
msg = nil
|
145
|
+
client.on_403 do |ps, m|
|
146
|
+
params = ps
|
147
|
+
msg = m
|
148
|
+
end
|
149
|
+
@socket.server_write '403 these are :my various arguments'
|
150
|
+
params.must_equal ['these', 'are', 'my various arguments']
|
151
|
+
msg.command.must_equal '403'
|
152
|
+
msg.source.nick.must_equal 'server1.example.org'
|
153
|
+
end
|
154
|
+
|
155
|
+
it "triggers an event split across multiple reads" do
|
156
|
+
params = nil
|
157
|
+
msg = nil
|
158
|
+
client.on_ping do |ps, m|
|
159
|
+
params = ps
|
160
|
+
msg = m
|
161
|
+
end
|
162
|
+
|
163
|
+
@socket.server_write_raw ':server1.example.org PI'
|
164
|
+
@socket.server_write_raw 'NG :echo this bac'
|
165
|
+
@socket.server_write_raw "k to me\r\n"
|
166
|
+
params.must_equal ['echo this back to me']
|
167
|
+
msg.command.must_equal 'PING'
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require File.expand_path('../../spec_helper', __FILE__)
|
2
|
+
|
3
|
+
describe Ircbgb::Matcher do
|
4
|
+
def matcher str; Ircbgb::Matcher.new str; end
|
5
|
+
|
6
|
+
it "treats ? as any single character (/./)" do
|
7
|
+
match = matcher('?lAm?')
|
8
|
+
'flame'.must_match match
|
9
|
+
'bLAMe'.must_match match
|
10
|
+
'lLaMa'.must_match match
|
11
|
+
'claMer'.wont_match match
|
12
|
+
'lam'.wont_match match
|
13
|
+
'filame'.wont_match match
|
14
|
+
'filament'.wont_match match
|
15
|
+
end
|
16
|
+
|
17
|
+
it "treats * as 0 or more characters (/.*/)" do
|
18
|
+
match = matcher('*laM*')
|
19
|
+
'lam'.must_match match
|
20
|
+
'LaMabcdefghijklmnopqrstuvwxyz1234567890[]{}()!@#$%^&*'.must_match match
|
21
|
+
'abcdefghijklmnopqrstuvwxyz1234567890[]{}()!@#$%^&*lAM'.must_match match
|
22
|
+
'abcdefghijklmnopqrsLAMtuvwxyz1234567890[]{}()!@#$%^&*'.must_match match
|
23
|
+
'flMa'.wont_match match
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
require File.expand_path('../../spec_helper', __FILE__)
|
2
|
+
|
3
|
+
describe Ircbgb::MessageParser do
|
4
|
+
def parse_it str
|
5
|
+
Ircbgb::MessageParser.parse ":#{str}\r\n"
|
6
|
+
end
|
7
|
+
|
8
|
+
it "parses user message without args" do
|
9
|
+
msg = parse_it 'my_nick!~my|user_[nam{@some.host KiCk'
|
10
|
+
msg.source.nick.must_equal 'my_nick' # just to enusre we've got a user proper
|
11
|
+
msg.source.to_s.must_equal 'my_nick!~my|user_[nam{@some.host'
|
12
|
+
msg.command.must_equal 'KICK'
|
13
|
+
msg.params.must_equal []
|
14
|
+
end
|
15
|
+
|
16
|
+
it "parses user message with args" do
|
17
|
+
msg = parse_it '[slappy-!~figgle@scrappy.dappy.doo 919 a1,a11 b2.3 c3:88'
|
18
|
+
msg.source.to_s.must_equal '[slappy-!~figgle@scrappy.dappy.doo'
|
19
|
+
msg.command.must_equal '919'
|
20
|
+
msg.params.must_equal ['a1,a11', 'b2.3', 'c3:88']
|
21
|
+
end
|
22
|
+
|
23
|
+
it "parses user message with rest" do
|
24
|
+
msg = parse_it 'my_nick!my.user@my.host PRIVmsg :this maintains the spaces '
|
25
|
+
msg.source.to_s.must_equal 'my_nick!my.user@my.host'
|
26
|
+
msg.command.must_equal 'PRIVMSG'
|
27
|
+
msg.params.must_equal ['this maintains the spaces ']
|
28
|
+
end
|
29
|
+
|
30
|
+
it "parses user message with args and rest" do
|
31
|
+
msg = parse_it 'my_nick!my.user@my.host part #channel1,&channel2 limey :MOA:R SP:ACES!'
|
32
|
+
msg.source.to_s.must_equal 'my_nick!my.user@my.host'
|
33
|
+
msg.command.must_equal 'PART'
|
34
|
+
msg.params.must_equal ['#channel1,&channel2', 'limey', 'MOA:R SP:ACES!']
|
35
|
+
end
|
36
|
+
|
37
|
+
it "parses server message without args" do
|
38
|
+
msg = parse_it 'sweet.server 302'
|
39
|
+
msg.source.nick.must_equal 'sweet.server'
|
40
|
+
msg.source.to_s.must_equal 'sweet.server'
|
41
|
+
msg.command.must_equal '302'
|
42
|
+
msg.params.must_equal []
|
43
|
+
end
|
44
|
+
|
45
|
+
it "parses server message with args" do
|
46
|
+
msg = parse_it 'sweet.server noticed a1 a2,a3 a4:5'
|
47
|
+
msg.source.to_s.must_equal 'sweet.server'
|
48
|
+
msg.command.must_equal 'NOTICED'
|
49
|
+
msg.params.must_equal ['a1', 'a2,a3', 'a4:5']
|
50
|
+
end
|
51
|
+
|
52
|
+
it "parses server message with rest" do
|
53
|
+
msg = parse_it 'sweet.server quit :this has spaces !'
|
54
|
+
msg.source.to_s.must_equal 'sweet.server'
|
55
|
+
msg.command.must_equal 'QUIT'
|
56
|
+
msg.params.must_equal ['this has spaces !']
|
57
|
+
end
|
58
|
+
|
59
|
+
it "parses server message with args and rest" do
|
60
|
+
msg = parse_it 'sweet.server Pezz fright,:clap beddy pram :this : has : colons!'
|
61
|
+
msg.source.to_s.must_equal 'sweet.server'
|
62
|
+
msg.command.must_equal 'PEZZ'
|
63
|
+
msg.params.must_equal ['fright,:clap', 'beddy', 'pram', 'this : has : colons!']
|
64
|
+
end
|
65
|
+
|
66
|
+
it "parses ipv4 user hosts" do
|
67
|
+
msg = parse_it 'me!~notyou@123.45.67.89 mklame'
|
68
|
+
msg.source.host.must_equal '123.45.67.89'
|
69
|
+
end
|
70
|
+
|
71
|
+
it "parses ipv6 user hosts" do
|
72
|
+
msg = parse_it 'me!~notyou@00f:a221:03:e:1234:420b:f6:9a mklame'
|
73
|
+
msg.source.host.must_equal '00f:a221:03:e:1234:420b:f6:9a'
|
74
|
+
end
|
75
|
+
|
76
|
+
it "parses ipv4 over ipv6 hosts" do
|
77
|
+
msg = parse_it 'me!~notyou@0:0:0:0:0:0:1.2.3.4 mklame'
|
78
|
+
msg.source.host.must_equal '0:0:0:0:0:0:1.2.3.4'
|
79
|
+
msg = parse_it 'me!~notyou@0:0:0:0:0:FffF:1.2.3.4 mklame'
|
80
|
+
msg.source.host.must_equal '0:0:0:0:0:FffF:1.2.3.4'
|
81
|
+
end
|
82
|
+
|
83
|
+
it "does not parse malformed messages" do
|
84
|
+
lambda {
|
85
|
+
parse_it 'invalid'
|
86
|
+
}.must_raise ::Ircbgb::MessageFormatError
|
87
|
+
|
88
|
+
lambda {
|
89
|
+
parse_it 'nick!user@hostname.com '
|
90
|
+
}.must_raise ::Ircbgb::MessageFormatError
|
91
|
+
|
92
|
+
lambda {
|
93
|
+
parse_it ''
|
94
|
+
}.must_raise ::Ircbgb::MessageFormatError
|
95
|
+
|
96
|
+
lambda {
|
97
|
+
parse_it ' '
|
98
|
+
}.must_raise ::Ircbgb::MessageFormatError
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require File.expand_path('../../spec_helper', __FILE__)
|
2
|
+
|
3
|
+
describe Ircbgb::Message do
|
4
|
+
def client_message *args
|
5
|
+
@client_message ||= Ircbgb::Message.new(
|
6
|
+
Ircbgb::User.parse("nick!~user@some.host.name"),
|
7
|
+
'CMD',
|
8
|
+
['all', 'the', 'pretty little args']
|
9
|
+
)
|
10
|
+
end
|
11
|
+
|
12
|
+
def server_message
|
13
|
+
@server_message ||= Ircbgb::Message.new(
|
14
|
+
Ircbgb::Server.new('host.domain.tld'),
|
15
|
+
'303',
|
16
|
+
[]
|
17
|
+
)
|
18
|
+
end
|
19
|
+
|
20
|
+
it "parses the source" do
|
21
|
+
client_message.source.to_s.must_equal "nick!~user@some.host.name"
|
22
|
+
server_message.source.to_s.must_equal "host.domain.tld"
|
23
|
+
end
|
24
|
+
|
25
|
+
it "parses the command" do
|
26
|
+
client_message.command.must_equal 'CMD'
|
27
|
+
server_message.command.must_equal '303'
|
28
|
+
end
|
29
|
+
|
30
|
+
it "parses the parameters" do
|
31
|
+
client_message.params.must_equal ['all', 'the', 'pretty little args']
|
32
|
+
server_message.params.must_equal []
|
33
|
+
end
|
34
|
+
|
35
|
+
it "identifies a numeric reply" do
|
36
|
+
client_message.numeric?.must_equal false
|
37
|
+
server_message.numeric?.must_equal true
|
38
|
+
end
|
39
|
+
end
|