ircbgb 0.0.1.pre
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.
- 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
|