em-irc 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,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: