em-irc 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ .DS_Store
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm use 1.9.3
@@ -0,0 +1,12 @@
1
+ rvm:
2
+ - 1.9.2
3
+ - 1.9.3
4
+ bundler_args: --without darwin --without development
5
+ gemfile:
6
+ - Gemfile
7
+ before_script:
8
+ - "sudo apt-get -qq -y install ngircd"
9
+ - "service ngircd stop"
10
+ - "ngircd -f spec/config/ngircd-unencrypted.conf"
11
+ - "ngircd -f spec/config/ngircd-encrypted-gnutls.conf"
12
+ script: "bundle exec rake"
@@ -0,0 +1 @@
1
+ --no-private --protected - LICENSE
data/Gemfile ADDED
@@ -0,0 +1,23 @@
1
+ source 'http://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in em-irc.gemspec
4
+ gemspec
5
+
6
+ group :development do
7
+ gem 'pry'
8
+ gem 'yard'
9
+ gem 'redcarpet'
10
+ gem 'guard'
11
+ gem 'guard-rspec'
12
+ gem 'guard-bundler'
13
+ end
14
+
15
+ group :development, :test do
16
+ gem 'rake'
17
+ gem 'rspec', "~> 2"
18
+ end
19
+
20
+ group :darwin do
21
+ gem 'rb-fsevent'
22
+ gem 'growl'
23
+ end
@@ -0,0 +1,13 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ guard 'bundler' do
5
+ watch('Gemfile')
6
+ end
7
+
8
+ guard 'rspec', :version => 2 do
9
+ watch(%r{^spec/.+_spec\.rb$})
10
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
11
+ watch('lib/em-irc.rb') { "spec" }
12
+ watch('spec/spec_helper.rb') { "spec" }
13
+ end
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2012 Jerry Cheung.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
@@ -0,0 +1,100 @@
1
+ # EventMachine IRC Client
2
+
3
+ [CI Build Status](https://secure.travis-ci.org/jch/em-irc.png?branch=master)
4
+
5
+ em-irc is an IRC client that uses EventMachine to handle connections to servers.
6
+
7
+ ## Basic Usage
8
+
9
+ ````ruby
10
+ require 'em-irc'
11
+
12
+ client = EventMachine::IRC::Client.new do |c|
13
+ c.host = 'irc.freenode.net'
14
+ c.port = '6667'
15
+ c.nick = 'jch'
16
+
17
+ c.on(:connect) do
18
+ join('#general')
19
+ join('#private', 'key')
20
+ end
21
+
22
+ c.on(:join) do |channel| # called after joining a channel
23
+ message(channel, "howdy all")
24
+ end
25
+
26
+ c.on(:message) do |source, target, message| # called when being messaged
27
+ puts "<#{source}> -> <#{target}>: #{message}"
28
+ end
29
+
30
+ # callback for all messages sent from IRC server
31
+ c.on(:raw) do |hash|
32
+ puts "#{hash[:prefix]} #{hash[:command]} #{hash[:params].join(' ')}"
33
+ end
34
+ end
35
+
36
+ client.run! # start EventMachine loop
37
+ ````
38
+
39
+ ## Examples
40
+
41
+ In the examples folder, there are runnable examples.
42
+
43
+ * cli.rb - takes input from keyboard, outputs to stdout
44
+ * websocket.rb -
45
+ * echo.rb - bot that echos everything
46
+ * callback.rb - demonstrate how callbacks work
47
+
48
+ ## References
49
+
50
+ * [RFC 1459 - Internet Relay Chat Protocol](http://tools.ietf.org/html/rfc1459) overview of IRC architecture
51
+ * [RFC 2812 - Internet Relay Chat: Client Protocol](http://tools.ietf.org/html/rfc2812) specifics of client protocol
52
+ * [RFC 2813 - Internet Relay Chat: Server Protocol](http://tools.ietf.org/html/rfc2813) specifics of server protocol
53
+
54
+ ## Development
55
+
56
+ To run integration specs, you'll need to run a ssl and a non-ssl irc server locally.
57
+ On OSX, you can install a server via [Homebrew](http://mxcl.github.com/homebrew/) with:
58
+
59
+ ```
60
+ bundle
61
+ brew install ngircd
62
+ ngircd -f spec/config/ngircd-unencrypted.conf
63
+ ngircd -f spec/config/ngircd-encrypted-openssl.conf
64
+ bundle exec rake # or guard
65
+ ```
66
+
67
+ If the server is not starting up correctly, make sure you're ngircd is
68
+ compiled with openssl support rather than gnutls. You can see the server
69
+ boot output by passing the '-n' flag. Also not that travis-ci builds
70
+ are executed with gnutls.
71
+
72
+ ## <a name="license"></a>License
73
+
74
+ Copyright (c) 2012 Jerry Cheung.
75
+
76
+ Permission is hereby granted, free of charge, to any person obtaining a copy
77
+ of this software and associated documentation files (the "Software"), to deal
78
+ in the Software without restriction, including without limitation the rights
79
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
80
+ copies of the Software, and to permit persons to whom the Software is
81
+ furnished to do so, subject to the following conditions:
82
+
83
+ The above copyright notice and this permission notice shall be included in
84
+ all copies or substantial portions of the Software.
85
+
86
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
87
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
88
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
89
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
90
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
91
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
92
+ THE SOFTWARE.
93
+
94
+ ## TODO
95
+
96
+ * can we skip using Dispatcher connection handler class?
97
+ * extract :on, :trigger callback gem that works on instances. [hook](https://github.com/apotonick/hooks), but works with instances
98
+ * would prefer the interface to look synchronous, but work async
99
+ * ssl dispatcher testing
100
+ * speed up integration specs
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+ Bundler::GemHelper.install_tasks
4
+ require 'rspec/core/rake_task'
5
+ RSpec::Core::RakeTask.new(:spec)
6
+ task :default => :spec
@@ -0,0 +1,20 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/em-irc/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Jerry Cheung"]
6
+ gem.email = ["jch@whatcodecraves.com"]
7
+ gem.description = %q{em-irc is an IRC client that uses EventMachine to handle connections to servers}
8
+ gem.summary = %q{em-irc is an IRC client that uses EventMachine to handle connections to servers}
9
+ gem.homepage = "http://github.com/jch/em-irc"
10
+
11
+ gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
12
+ gem.files = `git ls-files`.split("\n")
13
+ gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
14
+ gem.name = "em-irc"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = EventMachine::IRC::VERSION
17
+
18
+ gem.add_runtime_dependency 'eventmachine'
19
+ gem.add_runtime_dependency 'activesupport'
20
+ end
@@ -0,0 +1,24 @@
1
+ require File.expand_path '../../lib/em-irc', __FILE__
2
+ require 'logger'
3
+
4
+ client = EventMachine::IRC::Client.new do
5
+ host '127.0.0.1'
6
+ port '6667'
7
+ # logger Logger.new(STDOUT)
8
+
9
+ on :connect do
10
+ puts 'client connected'
11
+ join("#general")
12
+ end
13
+
14
+ on :message do |source, target, message|
15
+ puts "#{target} <#{source}> #{message}"
16
+ end
17
+
18
+ # on :raw do |m|
19
+ # # puts "raw message: #{m.inspect}"
20
+ # end
21
+ end
22
+
23
+ # puts client.callbacks
24
+ client.run!
@@ -0,0 +1,18 @@
1
+ require 'bundler'
2
+ Bundler.setup :default
3
+
4
+ require 'active_support/core_ext/hash/keys'
5
+ require 'active_support/core_ext/object/blank'
6
+ require 'active_support/callbacks'
7
+ require 'eventmachine'
8
+ require 'forwardable'
9
+ require 'set'
10
+
11
+ $:.unshift File.expand_path '..', __FILE__
12
+
13
+ module EventMachine
14
+ module IRC
15
+ autoload :Client, 'em-irc/client'
16
+ autoload :Dispatcher, 'em-irc/dispatcher'
17
+ end
18
+ end
@@ -0,0 +1,215 @@
1
+ module EventMachine
2
+ module IRC
3
+ class Client
4
+ # EventMachine::Connection object to IRC server
5
+ # @private
6
+ attr_accessor :conn
7
+
8
+ # IRC server to connect to. Defaults to 127.0.0.1:6667
9
+ attr_accessor :host, :port
10
+
11
+ attr_accessor :nick
12
+ attr_accessor :realname
13
+ attr_accessor :ssl
14
+
15
+ # Custom logger
16
+ attr_accessor :logger
17
+
18
+ # Set of channels that this client is connected to
19
+ # @private
20
+ attr_reader :channels
21
+
22
+ # Hash of callbacks on events. key is symbol event name.
23
+ # value is array of procs to call
24
+ # @private
25
+ attr_reader :callbacks
26
+
27
+ # Build a new unconnected IRC client
28
+ #
29
+ # @param [Hash] options
30
+ # @option options [String] :host
31
+ # @option options [String] :port
32
+ # @option options [Boolean] :ssl
33
+ # @option options [String] :nick
34
+ # @option options [String] :realname
35
+ #
36
+ # @yield [client] new instance for decoration
37
+ def initialize(options = {}, &blk)
38
+ options.symbolize_keys!
39
+ options = {
40
+ host: '127.0.0.1',
41
+ port: '6667',
42
+ ssl: false,
43
+ realname: 'Anonymous Annie',
44
+ nick: "guest-#{Time.now.to_i % 1000}"
45
+ }.merge!(options)
46
+
47
+ @host = options[:host]
48
+ @port = options[:port]
49
+ @ssl = options[:ssl]
50
+ @realname = options[:realname]
51
+ @nick = options[:nick]
52
+ @channels = Set.new
53
+ @callbacks = Hash.new
54
+ @connected = false
55
+ yield self if block_given?
56
+ end
57
+
58
+ # Creates a Eventmachine TCP connection with :host and :port. It should be called
59
+ # after callbacks are registered.
60
+ # @see #on
61
+ # @return [EventMachine::Connection]
62
+ def connect
63
+ self.conn ||= EventMachine::connect(@host, @port, Dispatcher, parent: self)
64
+ end
65
+
66
+ # @return [Boolean]
67
+ def connected?
68
+ @connected
69
+ end
70
+
71
+ # Callbacks
72
+
73
+ # Register a callback with :name as one of the following, and
74
+ # a block with the same number of params.
75
+ #
76
+ # @example
77
+ # on(:join) {|channel| puts channel}
78
+ #
79
+ # :connect - called after connection to server established
80
+ #
81
+ # :join
82
+ # @param who [String]
83
+ # @param channel [String]
84
+ # @param names [Array]
85
+ #
86
+ # :message, :privmsg - called on channel message or nick message
87
+ # @param source [String]
88
+ # @param target [String]
89
+ # @param message [String]
90
+ #
91
+ # :raw - called for all messages from server
92
+ # @param raw_hash [Hash] same format as return of #parse_message
93
+ def on(name, &blk)
94
+ # TODO: I thought Hash.new([]) would work, but it gets empted out
95
+ # TODO: normalize aliases :privmsg, :message, etc
96
+ (@callbacks[name.to_sym] ||= []) << blk
97
+ end
98
+
99
+ # Trigger a named callback
100
+ def trigger(name, *args)
101
+ # TODO: should this be instance_eval(&blk)? prevents it from non-dsl style
102
+ (@callbacks[name.to_sym] || []).each {|blk| blk.call(*args)}
103
+ end
104
+
105
+ # Sends raw message to IRC server. Assumes message is correctly formatted
106
+ # TODO: what if connect fails? or disconnects?
107
+ def send_data(message)
108
+ return false unless connected?
109
+ message = message + "\r\n"
110
+ log Logger::DEBUG, message
111
+ self.conn.send_data(message)
112
+ end
113
+
114
+ # Client commands
115
+ # See [RFC 2812](http://tools.ietf.org/html/rfc2812)
116
+ def renick(nick)
117
+ send_data("NICK #{nick}")
118
+ end
119
+
120
+ def user(username, mode, realname)
121
+ send_data("USER #{username} #{mode} * :#{realname}")
122
+ end
123
+
124
+ def join(channel_name, channel_key = nil)
125
+ send_data("JOIN #{channel_name} #{channel_key}".strip)
126
+ end
127
+
128
+ def pong(servername)
129
+ send_data("PONG :#{servername}")
130
+ end
131
+
132
+ # @param target [String] nick or channel name
133
+ # @param message [String]
134
+ def privmsg(target, message)
135
+ send_data("PRIVMSG #{target} :#{message}")
136
+ end
137
+ alias_method :message, :privmsg
138
+
139
+ def quit(message = 'leaving')
140
+ send_data("QUIT :#{message}")
141
+ end
142
+
143
+ # @return [Hash] h
144
+ # @option h [String] :prefix
145
+ # @option h [String] :command
146
+ # @option h [Array] :params
147
+ # @private
148
+ def parse_message(message)
149
+ # TODO: error handling
150
+ result = {}
151
+ parts = message.split(' ')
152
+ result[:prefix] = parts.shift.gsub(/^:/, '') if parts[0] =~ /^:/
153
+ result[:command] = parts[0] # cleanup?
154
+ result[:params] = parts.slice(1..-1).map {|s| s.gsub(/^:/, '')}
155
+ result
156
+ end
157
+
158
+ def handle_parsed_message(m)
159
+ case m[:command]
160
+ when '001' # welcome message
161
+ when 'PING'
162
+ pong(m[:params].first)
163
+ trigger(:ping, *m[:params])
164
+ when 'PRIVMSG'
165
+ trigger(:message, m[:prefix], m[:params].first, m[:params].slice(1..-1).join(' '))
166
+ when 'QUIT'
167
+ when 'JOIN'
168
+ trigger(:join, m[:prefix], m[:params].first)
169
+ else
170
+ # noop
171
+ # {:prefix=>"irc.the.net", :command=>"433", :params=>["*", "one", "Nickname", "already", "in", "use", "irc.the.net", "451", "*", "Connection", "not", "registered"]}
172
+ # {:prefix=>"irc.the.net", :command=>"432", :params=>["*", "one_1328243723", "Erroneous", "nickname"]}
173
+ end
174
+ trigger(:raw, m)
175
+ end
176
+
177
+ # EventMachine Callbacks
178
+ def receive_data(data)
179
+ data.split("\r\n").each do |message|
180
+ parsed = parse_message(message)
181
+ handle_parsed_message(parsed)
182
+ end
183
+ end
184
+
185
+ # @private
186
+ def ready
187
+ @connected = true
188
+ renick(@nick)
189
+ user(@nick, '0', @realname)
190
+ trigger(:connect)
191
+ end
192
+
193
+ # @private
194
+ def unbind
195
+ trigger(:disconnect)
196
+ end
197
+
198
+ def log(*args)
199
+ @logger.log(*args) if @logger
200
+ end
201
+
202
+ def run!
203
+ EM.epoll
204
+ EventMachine.run do
205
+ trap("TERM") { EM::stop }
206
+ trap("INT") { EM::stop }
207
+ connect
208
+ log Logger::INFO, "Starting IRC client..."
209
+ end
210
+ log Logger::INFO, "Stopping IRC client"
211
+ @logger.close if @logger
212
+ end
213
+ end
214
+ end
215
+ end
@@ -0,0 +1,33 @@
1
+ module EventMachine
2
+ module IRC
3
+ # EventMachine connection handler class that dispatches connections back to another object.
4
+ class Dispatcher < EventMachine::Connection
5
+ extend Forwardable
6
+ def_delegators :@parent, :receive_data, :unbind
7
+
8
+ def initialize(options)
9
+ raise ArgumentError.new(":parent parameter is required for EM#connect") unless options[:parent]
10
+ # TODO: if parent doesn't respond to a above methods, do a no-op
11
+ @parent = options[:parent]
12
+ end
13
+
14
+ # @parent.conn is set back to nil when this is created
15
+ def post_init
16
+ @parent.conn = self
17
+ @parent.ready unless @parent.ssl
18
+ end
19
+
20
+ def connection_completed
21
+ if @parent.ssl
22
+ start_tls
23
+ else
24
+ @parent.ready
25
+ end
26
+ end
27
+
28
+ def ssl_handshake_completed
29
+ @parent.ready if @parent.ssl
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,5 @@
1
+ module EventMachine
2
+ module IRC
3
+ VERSION = '0.0.1'
4
+ end
5
+ end
@@ -0,0 +1,18 @@
1
+ -----BEGIN CERTIFICATE-----
2
+ MIIC4zCCAc2gAwIBAgIETzYPfjALBgkqhkiG9w0BAQUwHjELMAkGA1UEBhMCVVMx
3
+ DzANBgNVBAoTBmVtLWlyYzAeFw0xMjAyMTEwNjQ5MzdaFw0zOTA2MjgwNjQ5NTJa
4
+ MB4xCzAJBgNVBAYTAlVTMQ8wDQYDVQQKEwZlbS1pcmMwggEgMAsGCSqGSIb3DQEB
5
+ AQOCAQ8AMIIBCgKCAQEAzG8N4DlzIqX2lcM5DY9uNh0YrRmG0SJ9C2P5cl/sx+X/
6
+ sEjlqJweGJMDj4Q7PYGhlGux6iSX/4hzZ5/FtZADrlCTGR7wUBLznWug2yHQQa24
7
+ PfA5rtzwKCFUClQbWiDnOuiin7jx0o5gjvpLqq2c9x5iqO+IWJgMxH8LtGmv2dGN
8
+ zaffWBAfTnF7M+96IlZ7zaDLodWrwqX5QOqNAHP3RvhK7PXzaR1QrwcyuwdcV4Or
9
+ MKg+Vky92ixl/90+HqdDCMjhT7Attxrulc9Q2zxk1ZGKPYtbe4YuXDal4Zh5ODgD
10
+ rXXnN1wbYiQfHuOZc18nQ2EHtdfXXmFMxozyPszAKQIDAQABoy8wLTAMBgNVHRMB
11
+ Af8EAjAAMB0GA1UdDgQWBBTmTq5dq1pGlorhDvO4YEnh5U/waTALBgkqhkiG9w0B
12
+ AQUDggEBAFTnrOYZW5/u4XVKrRH40+Y+BlBj6EYHd4ewELJHPXudZzAUuxTm+X9Z
13
+ +EQBa72jCFa76tnyUDK/EffL5mdWCoTULIfIcCpJNrY/E9kpCBKL/0ium35f4QpP
14
+ Zc6xzZGZSsJsLSPHyAfdycVv6VGbRw08HCD5U+YSEKLXSpiW9jinQ8Lft0cYki6f
15
+ +d4wZCxu75uUrTlfRVsHPnEufRt3jQSxPNOTHy59xQGELnDgq66hn8MRPEIb37Em
16
+ rnyTF3RRR+Zcl4jgot14h0e7WdPhqFvf/l8xO/A17vDhnRFDqwROxITNnj9Ebww3
17
+ 8DSCjoUFVQP16iBhrUkDW+blkSxI3+4=
18
+ -----END CERTIFICATE-----
@@ -0,0 +1,27 @@
1
+ -----BEGIN RSA PRIVATE KEY-----
2
+ MIIEpAIBAAKCAQEAzG8N4DlzIqX2lcM5DY9uNh0YrRmG0SJ9C2P5cl/sx+X/sEjl
3
+ qJweGJMDj4Q7PYGhlGux6iSX/4hzZ5/FtZADrlCTGR7wUBLznWug2yHQQa24PfA5
4
+ rtzwKCFUClQbWiDnOuiin7jx0o5gjvpLqq2c9x5iqO+IWJgMxH8LtGmv2dGNzaff
5
+ WBAfTnF7M+96IlZ7zaDLodWrwqX5QOqNAHP3RvhK7PXzaR1QrwcyuwdcV4OrMKg+
6
+ Vky92ixl/90+HqdDCMjhT7Attxrulc9Q2zxk1ZGKPYtbe4YuXDal4Zh5ODgDrXXn
7
+ N1wbYiQfHuOZc18nQ2EHtdfXXmFMxozyPszAKQIDAQABAoIBACG9VF9va9qfOcP8
8
+ InmOn08MB0tjnUtSc1Zta2Oz2YV2UU2GrRc3ni9KbXnYBi+U/5YEyNS5XGKpN6QL
9
+ C5t3B2corNZRCQgA0LcS5nXerfB2W3l8IQZZ4sLkw+opI73WFK41ax2MQvuP/Eys
10
+ miSy9DxusIlsGvxwKm9FqSi0kU1pNjxcpQMHk4JLGliOLiFugCUe7CP75LI0i4oT
11
+ 5jbsCXvNgpzJ/P2hjzo6LTcpvbKuTZwJbNFPtg16YOGAm+sbom3vpgx36F0a3nqn
12
+ aX2ODPZ13TMkPIbGt2eX7MXUXZrGd4Lnf42YRNemS32LuQJvAv1Ydk2ZCgucgM2f
13
+ qpa//9cCgYEA17qONXG3BLz3YhnTwS3U370Iv4YZiW0F+MQUOvtBuJ1H5ley1vhv
14
+ 7FtqLyzkiPrso22mogBdl06iEnAW4ACTF0iVFf86zV6kAu5OCUn/OK6Jq6i6wyK4
15
+ WLQ178sJBfpDPQ9y9a16IJelweE2oOOrQgntdeExhcBgoQueEbDPRmsCgYEA8pi5
16
+ 7K8AbPz8yHfjpuG477SkHqyjoBPmf/1bfKm1X4m1mxqOOD1Z+fZl6l46gU06roIu
17
+ mH7uT0MJxINtnAtaHBm2wUu/4lTYfbHl38tpIMOJkFmB8QMLUAaW1JozNGI1105x
18
+ RjCfPpc3jKPx93L9GGYLKlX1BzloSN9qHugg8LsCgYBTRTZ5WLCNiqdu3YtISPmf
19
+ d2c2Dnwy+LtSX9VzQuMGcOd8+SMWKYWCVXLyTMDWQw0utDea1stJiVe0CEI8Ktyc
20
+ Iy8w83juJvbmDrhei4qRhYWslg+pHPDNhJpBOjz8arKjkiAMxu6aQA8CfH1Ksza6
21
+ 4fwgAVHNUAm8gDB+oaIQiwKBgQDdsmp+fHMEJrIPtVhKoo7yJ/+vtI8Xc/g6UNtx
22
+ clm4xE09QChmBtMaFm2On6wRi/UrkvZoD99See4MMxtQ9iLT+T/FJ6dke6sYOyHa
23
+ wbYB5g/p5ZJVITYOXOcrxPs0TOftKddkkHyGo4R1N0Gho+jdiit79e+lOXYu2lTN
24
+ h87KjwKBgQCCOgAQ38xWCNOZaQdprijGaP0VVVefj1lKrQQeG+hmiD5YdQdfML+t
25
+ XKnKaBBNVo3wEdSlXIXJs0gD4zN2QTzwk4NTYZX3pzTzX/hpj+A+S3W9x7yRsj0K
26
+ LS4L+gPhPNI/fl/t6s8CPTjx+mDTzyUY/Qlq9H8h8SqkEoMivEB7Pw==
27
+ -----END RSA PRIVATE KEY-----
@@ -0,0 +1,6 @@
1
+ [Global]
2
+ Name = irc.the.net
3
+ Info = Server Info Text
4
+ SSLPorts = 16697
5
+ SSLKeyFile = spec/config/gnutls-key.pem
6
+ SSLCertFile = spec/config/gnutls-cert.pem
@@ -0,0 +1,7 @@
1
+ [Global]
2
+ Name = irc.the.net
3
+ Info = Server Info Text
4
+ SSLPorts = 16697
5
+ SSLKeyFile = spec/config/openssl-key.pem
6
+ SSLCertFile = spec/config/openssl-cert.pem
7
+ SSLKeyFilePassword = asdf
@@ -0,0 +1,5 @@
1
+ [Global]
2
+ Name = irc.the.net
3
+ Info = Server Info Text
4
+ Ports = 16667
5
+
@@ -0,0 +1,22 @@
1
+ -----BEGIN CERTIFICATE-----
2
+ MIIDmjCCAoKgAwIBAgIJALx9REw11VpDMA0GCSqGSIb3DQEBBQUAMDwxCzAJBgNV
3
+ BAYTAkFVMQowCAYDVQQIFAFcMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0
4
+ eSBMdGQwHhcNMTIwMjExMDY1ODA0WhcNMzkwNjI4MDY1ODA0WjA8MQswCQYDVQQG
5
+ EwJBVTEKMAgGA1UECBQBXDEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0cyBQdHkg
6
+ THRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAw6G6SIFPMwPe3mfq
7
+ DmfwCrMYNgPJNeKq/ZENTJpnfLES7OGestfoqtNccNnVhlkBdxBsRJdv0WQVpnLt
8
+ rfCJw/y1928v4HHfJy55Yeap/CLX961LlFo0kbKM2wZnt7y7li2gIUJxhKfY4Jqj
9
+ fp4alAmbWf6I0V3BcStdOjbnDNo1SPkWbpGPbpI/uW2G5u3EbR4elnd5h7NLFHko
10
+ TuZG7z6t7nYXIGQlw2BZULC3Eb7lqR7uEVMDT4fIC09dVcfqRSX/ds571EEZI9W/
11
+ a2BuomsSiuVKoHWpo4ex3W25ENK+XShK/MaLktbe2mOOJUpmgfP/lDrNbgnlh7rP
12
+ y5HDgwIDAQABo4GeMIGbMB0GA1UdDgQWBBRF85veUxti8yTf1qO1z28yL31IcDBs
13
+ BgNVHSMEZTBjgBRF85veUxti8yTf1qO1z28yL31IcKFApD4wPDELMAkGA1UEBhMC
14
+ QVUxCjAIBgNVBAgUAVwxITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0
15
+ ZIIJALx9REw11VpDMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAKOs
16
+ xQQY12mnVEy9qGcHhONaCry0RlA9HR4oAU3MMiif8TOSyiruOLzoY78TOUfJVU6T
17
+ r70GjVBtVTr2l1L5NKSXxHEIVBgy5vKQCEjN47MaPrNpDhIBznS5Wgfnr47ybwrR
18
+ BA+Pn2vXLm5BP+7QQwY1LJxnkVTB11ei2x3PZcDyrG8iledFWitVEDZAWf5FXxRl
19
+ avoJrCYZwkyPgbULRfhDb80tJs5Snw1+31xMgPLGWLvfYPraqeLulp12JX8XrYUk
20
+ S+MyuDgxj2F8gaP2ixvHGyV583/EtvSSqUVa+QfSzeqy8Xg5eNzJJUYSApmIEVND
21
+ 8HrT37aWv/gsXauXrm8=
22
+ -----END CERTIFICATE-----
@@ -0,0 +1,30 @@
1
+ -----BEGIN RSA PRIVATE KEY-----
2
+ Proc-Type: 4,ENCRYPTED
3
+ DEK-Info: DES-EDE3-CBC,DC1390A647851FB0
4
+
5
+ HU95ipOFXkHgGMhgJJU5D519zvX8sT9o0ooSt36a+t4t1yUZ8tJ8czpeup/xjR0K
6
+ ej86uHhNgOjvvAGJ9bIRAgrjmLNqw5HAG017j4bPVKT1UAOtPwPRulC8nmrm8iQb
7
+ bO30ZjC/2w46VvZewipOQTF+8nvrj90ZuiwPGW+JQkMP4PLvfSH38006/OGw5iMI
8
+ lr7BMXmhWg0bw/Ow8kWEVHufUK6RASerb08dwzoxz1z4DT2QbNeaRS6VGoLZ94V/
9
+ oD1f8dR2A5Y513vBROsENbUZibBVhRpPgI3Q+5/SZPilHfXaGUEDFh1umBTcBCPX
10
+ O/zXPMyzrmk5IehdGMq7lFjqK1Betgc0zeWYoB/GLeNZGoP287fTrq1c0Kz9+FcT
11
+ 1EpDPjMAgrU16KG5gr7UZ3PZfifJZBzrdF8Ulleju2vb7edYT69D1xruAd+Ih7ge
12
+ AsJmFByQ/++LVidaDTTM/h+ZgVjXuzofLowAXvGr71YKFO7qUTfA/Vq+R08nCYQA
13
+ m5oKr3cXEW1uvjtVxyFk3HLlvgVTveYpSc+yNSCXYT4dT17gEuwg0GU0e2RV5cvQ
14
+ AqSfVj7Gs+sVnrHEbrqFlh2P1l24cEiAY9a+odWFQYN+frbgdqO5JC56AzbV67Ds
15
+ E6H3PSjvm4At3KklSzf8HLAfFZISRZ6kBoF5pRv4f5Np8I/3yrs8QYF7GZ7sY/w4
16
+ EdJHS6GesESpik2PWRYCNfgb7hV893d/pT8W3oRwRQfUjoqIYJfSER+n44gUf7oW
17
+ vj4Z3CPTntwDnqhMcgSg7BWsTI9OLeSPsErSwdKOqXDrnErkVpyRlVR1uJitE4BV
18
+ KDNri7rHxPqR8QmodstBP/eiBYqG27hQIFWXkBvsuZyb8fv3hKfpVUYVRXzQG+r+
19
+ z7VDN05AMAj4iEPh3YAG1cPPYIxaYd/CcmmkIADaCLiLIWPucb0bNJpMlowtMvbc
20
+ zmV10CUofVXpi3lqsIdkrMLYI2dPRx4QF2+COOcngU1PdvY/lTddQh+i6JSRTSKn
21
+ S2pdp+x22/Ipk5IgvxWpmZxTav1SnbZ7k48bZ5MlqQQRNTH0M8j4mhpUUfsup746
22
+ rMo0UbtR/jWAL5dIJzinjupBtdwyyfC5rb23XQeQvLynLW/kqfYUtrt1AkQZDpPV
23
+ aPNMQCqYdC+ly2PuXiXX/0MAE8K0XsgL5Brnui52XXNBnzZLwk4mrUgngKFkaSB6
24
+ nazqLutGw81vYV1TbEcijJMiGBQQaOaii/dSc26NOi/5dIlOlASEyUzjbcE2RX0M
25
+ TsbGYa7cpG1ELzTj5oCTszuTwiOcWg8mZXIrocGJCC6TMsK2p4ptVwQLKLNNhMLy
26
+ CLbIb327a73Pt4sleZ4aVEsc4kwuUjqytoZdWFmTuccV3tJFPgSHpAlQySy3Y0TM
27
+ L6IipocIQqSi+79ymw6FI6D0L2Ka7BDQsfd3pHO0GAFNMO9oye7rhTY9hQJyJNMV
28
+ oq1tBA44VTLbd1zkfIbSUa6GfHuZz6ITJE0+GpI3rfh2qtJFcpeMvOAaZHq/uxBq
29
+ 3+yLqxXDAIO0XyBY5R5zExMNkqfrgeDE2dl4rjtn0a8eggwsO5VrBrsr5UkNZ/4o
30
+ -----END RSA PRIVATE KEY-----
@@ -0,0 +1,80 @@
1
+ require 'spec_helper'
2
+
3
+ # add a raw message queue for debugging, might be a good normal feature
4
+ class TestClient < EventMachine::IRC::Client
5
+ def initialize(options = {})
6
+ super(options)
7
+ @received_messages = []
8
+ end
9
+
10
+ def receive_data(data)
11
+ @received_messages << data
12
+ super(data)
13
+ end
14
+
15
+ def history
16
+ "\n" + @received_messages.join("\n") + "\n"
17
+ end
18
+ end
19
+
20
+ shared_examples_for "integration" do
21
+ it 'should work' do
22
+ dan = client(nick: 'dan')
23
+ bob = client(nick: 'bob')
24
+
25
+ dan.on(:connect) {dan.join('#americano-test')}
26
+ bob.on(:connect) {bob.join('#americano-test')}
27
+
28
+ bob.on(:join) do |who, channel|
29
+ bob.message(channel, "dan: hello bob")
30
+ # bob.quit
31
+ # dan.quit
32
+ end
33
+
34
+ EM.run {
35
+ dan.connect
36
+ EM::add_timer(2) {bob.connect}
37
+ EM::add_timer(5) {EM::stop}
38
+ }
39
+
40
+ # TODO: matchers for commands
41
+ dan.history.should =~ /Welcome/
42
+ dan.history.should =~ /JOIN :#americano-test/
43
+ dan.history.should =~ /dan: hello bob/
44
+
45
+ bob.history.should =~ /Welcome/
46
+ bob.history.should =~ /JOIN :#americano-test/
47
+ end
48
+ end
49
+
50
+ # Assumes there is an IRC server running at localhost 6667
51
+ describe EventMachine::IRC::Client, :integration => true do
52
+ let(:options) do
53
+ {
54
+ host: '127.0.0.1',
55
+ port: '16667'
56
+ }.merge(@options || {})
57
+ end
58
+
59
+ def client(opts = {})
60
+ TestClient.new(options.merge(opts))
61
+ end
62
+
63
+ context 'non-ssl' do
64
+ before :all do
65
+ raise "unencrypted ircd not on :16667" unless `lsof -i :16667`.chomp.size > 1
66
+ end
67
+ it_behaves_like "integration"
68
+ end
69
+
70
+ context 'ssl' do
71
+ before :all do
72
+ raise "encrypted ircd not on :16697" unless `lsof -i :16697`.chomp.size > 1
73
+ @options = {
74
+ port: '16697',
75
+ ssl: true
76
+ }
77
+ end
78
+ it_behaves_like "integration"
79
+ end
80
+ end
@@ -0,0 +1,185 @@
1
+ require 'spec_helper'
2
+
3
+ describe EventMachine::IRC::Client do
4
+ context 'configuration' do
5
+ it 'defaults host to 127.0.0.1' do
6
+ subject.host.should == '127.0.0.1'
7
+ end
8
+
9
+ it 'defaults realname to random generated name' do
10
+ subject.realname.should_not be_blank
11
+ end
12
+
13
+ it 'defaults port to 6667' do
14
+ subject.port.should == '6667'
15
+ end
16
+
17
+ it 'defaults ssl to false' do
18
+ subject.ssl.should == false
19
+ end
20
+
21
+ it 'defaults channels to an empty set' do
22
+ subject.channels.should be_empty
23
+ end
24
+
25
+ it 'should not be connected' do
26
+ subject.should_not be_connected
27
+ end
28
+
29
+ it 'should default a nick' do
30
+ subject.nick.should_not be_blank
31
+ end
32
+
33
+ it 'should yield self' do
34
+ client = described_class.new do |c|
35
+ c.should be_kind_of(described_class)
36
+ end
37
+ end
38
+
39
+ it 'should allow a custom logger' do
40
+ subject.logger = Logger.new(STDOUT)
41
+ subject.logger = nil
42
+ end
43
+ end
44
+
45
+ context 'connect' do
46
+ it 'should create an EM TCP connection with host, port, handler, and self' do
47
+ EventMachine.should_receive(:connect).with('irc.net', '9999', EventMachine::IRC::Dispatcher, parent: subject)
48
+ subject.host = 'irc.net'
49
+ subject.port = '9999'
50
+ subject.connect
51
+ end
52
+
53
+ it 'should be idempotent' do
54
+ EventMachine.stub(connect: mock('Connection'))
55
+ EventMachine.should_receive(:connect).exactly(1).times
56
+ subject.connect
57
+ subject.connect
58
+ end
59
+ end
60
+
61
+ context 'send_data' do
62
+ before do
63
+ @connection = mock('Connection')
64
+ subject.stub(conn: @connection)
65
+ subject.stub(connected?: true)
66
+ end
67
+
68
+ it 'should return false if not connected' do
69
+ subject.stub(connected?: nil)
70
+ subject.send_data("NICK jch").should be_false
71
+ end
72
+
73
+ it 'should send message to irc server' do
74
+ subject.stub(conn: @connection)
75
+ @connection.should_receive(:send_data).with("NICK jch\r\n")
76
+ subject.send_data("NICK jch")
77
+ end
78
+ end
79
+
80
+ context 'ready' do
81
+ before do
82
+ subject.stub(conn: mock.as_null_object)
83
+ end
84
+
85
+ it 'should call :connect callback' do
86
+ m = mock('callback')
87
+ m.should_receive(:callback)
88
+ subject.on(:connect) {m.callback}
89
+ subject.ready
90
+ end
91
+
92
+ it 'should mark client as connected' do
93
+ subject.ready
94
+ subject.should be_connected
95
+ end
96
+ end
97
+
98
+ context 'unbind' do
99
+ it 'should call :disconnect callback' do
100
+ m = mock('callback')
101
+ m.should_receive(:callback)
102
+ subject.on(:disconnect) {m.callback}
103
+ subject.unbind
104
+ end
105
+ end
106
+
107
+ context 'message parsing' do
108
+ context 'prefix' do
109
+ it 'should be optional' do
110
+ parsed = subject.parse_message('NICK jch')
111
+ parsed[:prefix].should be_nil
112
+ end
113
+
114
+ it 'should start with :' do
115
+ parsed = subject.parse_message(':jch!host 123 :params')
116
+ parsed[:prefix].should == 'jch!host'
117
+ end
118
+ end
119
+
120
+ context 'params' do
121
+ it 'should remove leading :' do
122
+ parsed = subject.parse_message('PING :irc.net')
123
+ parsed[:params] =~ ['irc.net']
124
+ end
125
+ end
126
+ end
127
+
128
+ context 'receive_data' do
129
+ before do
130
+ subject.stub(:parse_message).and_return(mock.as_null_object)
131
+ subject.stub(:handle_parsed_message).and_return(mock.as_null_object)
132
+ end
133
+
134
+ it 'should parse messages separated by \r\n' do
135
+ data = [
136
+ ":irc.the.net 001 jessie :Welcome to the Internet Relay Network jessie!~jessie@localhost",
137
+ ":irc.the.net 002 jessie :Your host is irc.the.net, running version ngircd-17.1 (i386/apple/darwin11.2.0)",
138
+ ":irc.the.net 003 jessie :This server has been started Fri Feb 03 2012 at 14:42:38 (PST)"
139
+ ].join("\r\n")
140
+
141
+ subject.should_receive(:parse_message).exactly(3).times
142
+ subject.should_receive(:handle_parsed_message).exactly(3).times
143
+ subject.receive_data(data)
144
+ end
145
+
146
+ it 'should handle parsed messages' do
147
+ end
148
+ end
149
+
150
+ context 'handle_parsed_message' do
151
+ it 'should respond to pings' do
152
+ subject.should_receive(:pong).with("irc.net")
153
+ subject.handle_parsed_message({prefix: 'irc.net', command: 'PING', params: ['irc.net']})
154
+ end
155
+
156
+ # TODO: do we want a delegate object and callbacks?
157
+ # it 'should call optional delegate' do
158
+ # subject.stub(delegate: mock('Delegate'))
159
+ # subject.delegate.should_receive(:message)
160
+ # subject.handle_parsed_message({prefix: 'jessie!jessie@localhost', command: 'PRIVMSG', params: ['#general', 'hello world'])
161
+ # end
162
+
163
+ it 'should trigger :raw callbacks' do
164
+ m = mock('Parsed Response').as_null_object
165
+ subject.should_receive(:trigger).with(:raw, m)
166
+ subject.handle_parsed_message(m)
167
+ end
168
+ end
169
+
170
+ context 'callbacks' do
171
+ it 'should register multiple' do
172
+ m = mock('Callback')
173
+ subject.on(:foo) {m.callback}
174
+ subject.on(:foo) {m.callback}
175
+ subject.callbacks[:foo].size.should == 2
176
+ end
177
+
178
+ it 'should trigger with params' do
179
+ m = mock('Callback')
180
+ m.should_receive(:callback).with('arg')
181
+ subject.on(:foo) {|arg| m.callback(arg)}
182
+ subject.trigger(:foo, 'arg')
183
+ end
184
+ end
185
+ end
@@ -0,0 +1,44 @@
1
+ require 'spec_helper'
2
+
3
+ shared_examples_for 'dispatcher' do
4
+ it 'should delegate connection methods to parent' do
5
+ parent = Class.new do
6
+ attr_accessor :conn
7
+ def ssl
8
+ $ssl
9
+ end
10
+
11
+ def receive_data(data)
12
+ EventMachine::stop_event_loop
13
+ raise "Didn't get expected message" unless data == 'message'
14
+ end
15
+ end.new
16
+ parent.should_receive(:ready)
17
+ parent.should_receive(:unbind)
18
+ EventMachine.run {
19
+ EventMachine::start_server('127.0.0.1', '198511', described_class, parent: parent)
20
+ EventMachine::connect('127.0.0.1', '198511') do |conn|
21
+ conn.send_data("message")
22
+ end
23
+ EventMachine::add_timer(2) {
24
+ EventMachine::stop_event_loop
25
+ raise "Never reached receive_data or took too long"
26
+ }
27
+ }
28
+ end
29
+ end
30
+
31
+ describe EventMachine::IRC::Dispatcher do
32
+ context 'ssl integration' do
33
+ before {$ssl = true}
34
+ it 'behaves like dispatcher' do
35
+ pending "not sure how to test ssl"
36
+ end
37
+ # it_behaves_like "dispatcher"
38
+ end
39
+
40
+ context 'non-ssl integration' do
41
+ before {$ssl = false}
42
+ it_behaves_like "dispatcher"
43
+ end
44
+ end
@@ -0,0 +1,6 @@
1
+ require File.expand_path '../lib/em-irc', File.dirname(__FILE__)
2
+ Bundler.require :test
3
+ require 'logger'
4
+
5
+ RSpec.configure do |c|
6
+ end
metadata ADDED
@@ -0,0 +1,107 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: em-irc
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Jerry Cheung
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-02-11 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: eventmachine
16
+ requirement: &70164206319840 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *70164206319840
25
+ - !ruby/object:Gem::Dependency
26
+ name: activesupport
27
+ requirement: &70164206319260 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *70164206319260
36
+ description: em-irc is an IRC client that uses EventMachine to handle connections
37
+ to servers
38
+ email:
39
+ - jch@whatcodecraves.com
40
+ executables: []
41
+ extensions: []
42
+ extra_rdoc_files: []
43
+ files:
44
+ - .gitignore
45
+ - .rspec
46
+ - .rvmrc
47
+ - .travis.yml
48
+ - .yardopts
49
+ - Gemfile
50
+ - Guardfile
51
+ - LICENSE
52
+ - README.md
53
+ - Rakefile
54
+ - em-irc.gemspec
55
+ - examples/logger.rb
56
+ - lib/em-irc.rb
57
+ - lib/em-irc/client.rb
58
+ - lib/em-irc/dispatcher.rb
59
+ - lib/em-irc/version.rb
60
+ - spec/config/gnutls-cert.pem
61
+ - spec/config/gnutls-key.pem
62
+ - spec/config/ngircd-encrypted-gnutls.conf
63
+ - spec/config/ngircd-encrypted-openssl.conf
64
+ - spec/config/ngircd-unencrypted.conf
65
+ - spec/config/openssl-cert.pem
66
+ - spec/config/openssl-key.pem
67
+ - spec/integration/integration_spec.rb
68
+ - spec/lib/em-irc/client_spec.rb
69
+ - spec/lib/em-irc/dispatcher_spec.rb
70
+ - spec/spec_helper.rb
71
+ homepage: http://github.com/jch/em-irc
72
+ licenses: []
73
+ post_install_message:
74
+ rdoc_options: []
75
+ require_paths:
76
+ - lib
77
+ required_ruby_version: !ruby/object:Gem::Requirement
78
+ none: false
79
+ requirements:
80
+ - - ! '>='
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ required_rubygems_version: !ruby/object:Gem::Requirement
84
+ none: false
85
+ requirements:
86
+ - - ! '>='
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ requirements: []
90
+ rubyforge_project:
91
+ rubygems_version: 1.8.10
92
+ signing_key:
93
+ specification_version: 3
94
+ summary: em-irc is an IRC client that uses EventMachine to handle connections to servers
95
+ test_files:
96
+ - spec/config/gnutls-cert.pem
97
+ - spec/config/gnutls-key.pem
98
+ - spec/config/ngircd-encrypted-gnutls.conf
99
+ - spec/config/ngircd-encrypted-openssl.conf
100
+ - spec/config/ngircd-unencrypted.conf
101
+ - spec/config/openssl-cert.pem
102
+ - spec/config/openssl-key.pem
103
+ - spec/integration/integration_spec.rb
104
+ - spec/lib/em-irc/client_spec.rb
105
+ - spec/lib/em-irc/dispatcher_spec.rb
106
+ - spec/spec_helper.rb
107
+ has_rdoc: