Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

initial commit

  • Loading branch information...
commit 52877768f876e73306abbc7e19677eb3db364914 0 parents
Jerry Cheung authored
18 .gitignore
@@ -0,0 +1,18 @@
+*.gem
+*.rbc
+.bundle
+.config
+.yardoc
+Gemfile.lock
+InstalledFiles
+_yardoc
+coverage
+doc/
+lib/bundler/man
+pkg
+rdoc
+spec/reports
+test/tmp
+test/version_tmp
+tmp
+.DS_Store
1  .rspec
@@ -0,0 +1 @@
+--color
1  .rvmrc
@@ -0,0 +1 @@
+rvm use 1.9.3
10 .travis.yml
@@ -0,0 +1,10 @@
+rvm:
+ - 1.9.2
+ - 1.9.3
+bundler_args: --without darwin
+gemfile:
+ - Gemfile
+before_script:
+ - "sudo apt-get install ngircd"
+ - "cd spec/config && ngircd -f ngircd-unencrypted.conf && ngircd -f ngircd-encrypted.conf"
+script: "rake spec"
1  .yardopts
@@ -0,0 +1 @@
+--no-private --protected - LICENSE
21 Gemfile
@@ -0,0 +1,21 @@
+source 'http://rubygems.org'
+
+# Specify your gem's dependencies in em-irc.gemspec
+gemspec
+
+group :development, :test do
+ gem 'rake'
+ gem 'rspec', "~> 2"
+ gem 'pry'
+ gem 'yard'
+ gem 'redcarpet'
+
+ gem 'guard'
+ gem 'guard-rspec'
+ gem 'guard-bundler'
+end
+
+group :darwin do
+ gem 'rb-fsevent'
+ gem 'growl'
+end
13 Guardfile
@@ -0,0 +1,13 @@
+# A sample Guardfile
+# More info at https://github.com/guard/guard#readme
+
+guard 'bundler' do
+ watch('Gemfile')
+end
+
+guard 'rspec', :version => 2 do
+ watch(%r{^spec/.+_spec\.rb$})
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
+ watch('lib/em-irc.rb') { "spec" }
+ watch('spec/spec_helper.rb') { "spec" }
+end
19 LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2012 Jerry Cheung.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
96 README.md
@@ -0,0 +1,96 @@
+# EventMachine IRC Client
+
+[![CI Build Status](https://secure.travis-ci.org/jch/em-irc.png?branch=master)]
+
+em-irc is an IRC client that uses EventMachine to handle connections to servers.
+
+## Basic Usage
+
+````ruby
+require 'em-irc'
+
+client = EventMachine::IRC::Client.new do |c|
+ c.host = 'irc.freenode.net'
+ c.port = '6667'
+ c.nick = 'jch'
+
+ c.on(:connect) do
+ join('#general')
+ join('#private', 'key')
+ end
+
+ c.on(:join) do |channel| # called after joining a channel
+ message(channel, "howdy all")
+ end
+
+ c.on(:message) do |source, target, message| # called when being messaged
+ puts "<#{source}> -> <#{target}>: #{message}"
+ end
+
+ # callback for all messages sent from IRC server
+ c.on(:raw) do |hash|
+ puts "#{hash[:prefix]} #{hash[:command]} #{hash[:params].join(' ')}"
+ end
+end
+
+client.run! # start EventMachine loop
+````
+
+## Examples
+
+In the examples folder, there are runnable examples.
+
+* cli.rb - takes input from keyboard, outputs to stdout
+* websocket.rb -
+* echo.rb - bot that echos everything
+* callback.rb - demonstrate how callbacks work
+
+## References
+
+* [RFC 1459 - Internet Relay Chat Protocol](http://tools.ietf.org/html/rfc1459) overview of IRC architecture
+* [RFC 2812 - Internet Relay Chat: Client Protocol](http://tools.ietf.org/html/rfc2812) specifics of client protocol
+* [RFC 2813 - Internet Relay Chat: Server Protocol](http://tools.ietf.org/html/rfc2813) specifics of server protocol
+
+## Development
+
+To run integration specs, you'll need to run a ssl and a non-ssl irc server locally.
+On OSX, you can install a server via [Homebrew](http://mxcl.github.com/homebrew/) with:
+
+```
+bundle
+brew install ngircd
+cd spec/config
+ngircd -f ngircd-unencrypted.conf
+ngircd -f ngircd-encrypted.conf
+rake # or guard
+```
+
+## <a name="license"></a>License
+
+Copyright (c) 2012 Jerry Cheung.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+## TODO
+
+* can we skip using Dispatcher connection handler class?
+* extract :on, :trigger callback gem that works on instances. [hook](https://github.com/apotonick/hooks), but works with instances
+* would prefer the interface to look synchronous, but work async
+* ssl dispatcher testing
+* speed up integration specs
6 Rakefile
@@ -0,0 +1,6 @@
+#!/usr/bin/env rake
+require "bundler/gem_tasks"
+Bundler::GemHelper.install_tasks
+require 'rspec/core/rake_task'
+RSpec::Core::RakeTask.new(:spec)
+task :default => :spec
20 em-irc.gemspec
@@ -0,0 +1,20 @@
+# -*- encoding: utf-8 -*-
+require File.expand_path('../lib/em-irc/version', __FILE__)
+
+Gem::Specification.new do |gem|
+ gem.authors = ["Jerry Cheung"]
+ gem.email = ["jch@whatcodecraves.com"]
+ gem.description = %q{em-irc is an IRC client that uses EventMachine to handle connections to servers}
+ gem.summary = %q{em-irc is an IRC client that uses EventMachine to handle connections to servers}
+ gem.homepage = "http://github.com/jch/em-irc"
+
+ gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
+ gem.files = `git ls-files`.split("\n")
+ gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
+ gem.name = "em-irc"
+ gem.require_paths = ["lib"]
+ gem.version = EventMachine::IRC::VERSION
+
+ gem.add_runtime_dependency 'eventmachine'
+ gem.add_runtime_dependency 'activesupport'
+end
24 examples/logger.rb
@@ -0,0 +1,24 @@
+require File.expand_path '../../lib/em-irc', __FILE__
+require 'logger'
+
+client = EventMachine::IRC::Client.new do
+ host '127.0.0.1'
+ port '6667'
+ # logger Logger.new(STDOUT)
+
+ on :connect do
+ puts 'client connected'
+ join("#general")
+ end
+
+ on :message do |source, target, message|
+ puts "#{target} <#{source}> #{message}"
+ end
+
+ # on :raw do |m|
+ # # puts "raw message: #{m.inspect}"
+ # end
+end
+
+# puts client.callbacks
+client.run!
18 lib/em-irc.rb
@@ -0,0 +1,18 @@
+require 'bundler'
+Bundler.setup :default
+
+require 'active_support/core_ext/hash/keys'
+require 'active_support/core_ext/object/blank'
+require 'active_support/callbacks'
+require 'eventmachine'
+require 'forwardable'
+require 'set'
+
+$:.unshift File.expand_path '..', __FILE__
+
+module EventMachine
+ module IRC
+ autoload :Client, 'em-irc/client'
+ autoload :Dispatcher, 'em-irc/dispatcher'
+ end
+end
215 lib/em-irc/client.rb
@@ -0,0 +1,215 @@
+module EventMachine
+ module IRC
+ class Client
+ # EventMachine::Connection object to IRC server
+ # @private
+ attr_accessor :conn
+
+ # IRC server to connect to. Defaults to 127.0.0.1:6667
+ attr_accessor :host, :port
+
+ attr_accessor :nick
+ attr_accessor :realname
+ attr_accessor :ssl
+
+ # Custom logger
+ attr_accessor :logger
+
+ # Set of channels that this client is connected to
+ # @private
+ attr_reader :channels
+
+ # Hash of callbacks on events. key is symbol event name.
+ # value is array of procs to call
+ # @private
+ attr_reader :callbacks
+
+ # Build a new unconnected IRC client
+ #
+ # @param [Hash] options
+ # @option options [String] :host
+ # @option options [String] :port
+ # @option options [Boolean] :ssl
+ # @option options [String] :nick
+ # @option options [String] :realname
+ #
+ # @yield [client] new instance for decoration
+ def initialize(options = {}, &blk)
+ options.symbolize_keys!
+ options = {
+ host: '127.0.0.1',
+ port: '6667',
+ ssl: false,
+ realname: 'Anonymous Annie',
+ nick: "guest-#{Time.now.to_i % 1000}"
+ }.merge!(options)
+
+ @host = options[:host]
+ @port = options[:port]
+ @ssl = options[:ssl]
+ @realname = options[:realname]
+ @nick = options[:nick]
+ @channels = Set.new
+ @callbacks = Hash.new
+ @connected = false
+ yield self if block_given?
+ end
+
+ # Creates a Eventmachine TCP connection with :host and :port. It should be called
+ # after callbacks are registered.
+ # @see #on
+ # @return [EventMachine::Connection]
+ def connect
+ self.conn ||= EventMachine::connect(@host, @port, Dispatcher, parent: self)
+ end
+
+ # @return [Boolean]
+ def connected?
+ @connected
+ end
+
+ # Callbacks
+
+ # Register a callback with :name as one of the following, and
+ # a block with the same number of params.
+ #
+ # @example
+ # on(:join) {|channel| puts channel}
+ #
+ # :connect - called after connection to server established
+ #
+ # :join
+ # @param who [String]
+ # @param channel [String]
+ # @param names [Array]
+ #
+ # :message, :privmsg - called on channel message or nick message
+ # @param source [String]
+ # @param target [String]
+ # @param message [String]
+ #
+ # :raw - called for all messages from server
+ # @param raw_hash [Hash] same format as return of #parse_message
+ def on(name, &blk)
+ # TODO: I thought Hash.new([]) would work, but it gets empted out
+ # TODO: normalize aliases :privmsg, :message, etc
+ (@callbacks[name.to_sym] ||= []) << blk
+ end
+
+ # Trigger a named callback
+ def trigger(name, *args)
+ # TODO: should this be instance_eval(&blk)? prevents it from non-dsl style
+ (@callbacks[name.to_sym] || []).each {|blk| blk.call(*args)}
+ end
+
+ # Sends raw message to IRC server. Assumes message is correctly formatted
+ # TODO: what if connect fails? or disconnects?
+ def send_data(message)
+ return false unless connected?
+ message = message + "\r\n"
+ log Logger::DEBUG, message
+ self.conn.send_data(message)
+ end
+
+ # Client commands
+ # See [RFC 2812](http://tools.ietf.org/html/rfc2812)
+ def renick(nick)
+ send_data("NICK #{nick}")
+ end
+
+ def user(username, mode, realname)
+ send_data("USER #{username} #{mode} * :#{realname}")
+ end
+
+ def join(channel_name, channel_key = nil)
+ send_data("JOIN #{channel_name} #{channel_key}".strip)
+ end
+
+ def pong(servername)
+ send_data("PONG :#{servername}")
+ end
+
+ # @param target [String] nick or channel name
+ # @param message [String]
+ def privmsg(target, message)
+ send_data("PRIVMSG #{target} :#{message}")
+ end
+ alias_method :message, :privmsg
+
+ def quit(message = 'leaving')
+ send_data("QUIT :#{message}")
+ end
+
+ # @return [Hash] h
+ # @option h [String] :prefix
+ # @option h [String] :command
+ # @option h [Array] :params
+ # @private
+ def parse_message(message)
+ # TODO: error handling
+ result = {}
+ parts = message.split(' ')
+ result[:prefix] = parts.shift.gsub(/^:/, '') if parts[0] =~ /^:/
+ result[:command] = parts[0] # cleanup?
+ result[:params] = parts.slice(1..-1).map {|s| s.gsub(/^:/, '')}
+ result
+ end
+
+ def handle_parsed_message(m)
+ case m[:command]
+ when '001' # welcome message
+ when 'PING'
+ pong(m[:params].first)
+ trigger(:ping, *m[:params])
+ when 'PRIVMSG'
+ trigger(:message, m[:prefix], m[:params].first, m[:params].slice(1..-1).join(' '))
+ when 'QUIT'
+ when 'JOIN'
+ trigger(:join, m[:prefix], m[:params].first)
+ else
+ # noop
+ # {:prefix=>"irc.the.net", :command=>"433", :params=>["*", "one", "Nickname", "already", "in", "use", "irc.the.net", "451", "*", "Connection", "not", "registered"]}
+ # {:prefix=>"irc.the.net", :command=>"432", :params=>["*", "one_1328243723", "Erroneous", "nickname"]}
+ end
+ trigger(:raw, m)
+ end
+
+ # EventMachine Callbacks
+ def receive_data(data)
+ data.split("\r\n").each do |message|
+ parsed = parse_message(message)
+ handle_parsed_message(parsed)
+ end
+ end
+
+ # @private
+ def ready
+ @connected = true
+ renick(@nick)
+ user(@nick, '0', @realname)
+ trigger(:connect)
+ end
+
+ # @private
+ def unbind
+ trigger(:disconnect)
+ end
+
+ def log(*args)
+ @logger.log(*args) if @logger
+ end
+
+ def run!
+ EM.epoll
+ EventMachine.run do
+ trap("TERM") { EM::stop }
+ trap("INT") { EM::stop }
+ connect
+ log Logger::INFO, "Starting IRC client..."
+ end
+ log Logger::INFO, "Stopping IRC client"
+ @logger.close if @logger
+ end
+ end
+ end
+end
33 lib/em-irc/dispatcher.rb
@@ -0,0 +1,33 @@
+module EventMachine
+ module IRC
+ # EventMachine connection handler class that dispatches connections back to another object.
+ class Dispatcher < EventMachine::Connection
+ extend Forwardable
+ def_delegators :@parent, :receive_data, :unbind
+
+ def initialize(options)
+ raise ArgumentError.new(":parent parameter is required for EM#connect") unless options[:parent]
+ # TODO: if parent doesn't respond to a above methods, do a no-op
+ @parent = options[:parent]
+ end
+
+ # @parent.conn is set back to nil when this is created
+ def post_init
+ @parent.conn = self
+ @parent.ready unless @parent.ssl
+ end
+
+ def connection_completed
+ if @parent.ssl
+ start_tls
+ else
+ @parent.ready
+ end
+ end
+
+ def ssl_handshake_completed
+ @parent.ready if @parent.ssl
+ end
+ end
+ end
+end
5 lib/em-irc/version.rb
@@ -0,0 +1,5 @@
+module EventMachine
+ module IRC
+ VERSION = '0.0.1'
+ end
+end
8 spec/config/ngircd-encrypted.conf
@@ -0,0 +1,8 @@
+[Global]
+ Name = irc.the.net
+ Info = Server Info Text
+ SSLPorts = 6697
+ SSLKeyFile = server-key.pem
+ SSLKeyFilePassword = asdf
+ SSLCertFile = server-cert.pem
+ SyslogFacility = local1
5 spec/config/ngircd-unencrypted.conf
@@ -0,0 +1,5 @@
+[Global]
+ Name = irc.the.net
+ Info = Server Info Text
+ Ports = 6667
+
22 spec/config/server-cert.pem
@@ -0,0 +1,22 @@
+-----BEGIN CERTIFICATE-----
+MIIDtTCCAp2gAwIBAgIJAKzMi7fTgTaMMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV
+BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX
+aWRnaXRzIFB0eSBMdGQwHhcNMTIwMjExMDIyMDE5WhcNMTYwMjExMDIyMDE5WjBF
+MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50
+ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
+CgKCAQEAxD/6VQlFVefkswFNsrkweGGsaHZJcKDBUZr6p3Gsgc9wzklUAeo+VEaL
+lcStljtUFq0+0WVR6jZMSZaw12ftnlOz62QzU6xHOqv0U1/yLhFWPhdHEypqAQGY
+QEyCq/U9IFnIAOASnCANv8US0F6C4Wwd4xOxWkt5Seob6ExVVKiv7e6WAB4fLZti
+WdolaMCWiS7pg/YSJ6VIrJvxt6M3kGNG7YaOCvabUOWzBroFeeGG+UuhGg/rsEUA
+GH/gq0nXR4TLoa6LnMjqDJQ6gq17RP5+dcsYZI71oQKeU9K/gODOL25AFZ/CDM85
+DCUet1GPsZbjjMSnjYysKrqXXHFmywIDAQABo4GnMIGkMB0GA1UdDgQWBBTbKUW9
+lfxlOBcejXia+Q5O973UuTB1BgNVHSMEbjBsgBTbKUW9lfxlOBcejXia+Q5O973U
+uaFJpEcwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgTClNvbWUtU3RhdGUxITAfBgNV
+BAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZIIJAKzMi7fTgTaMMAwGA1UdEwQF
+MAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAGvk458JQb5ocUGKjFgDGBRARX4doXTC
+ccRXS2X549O5AGMwqY5se7jWiHEdYsy7Ke9tXkw1wihq5n70Kxp2edZw1dZX3jwN
+0sq8Jc18R3TEXankyci/+3cFyg2c3FAfgXa7/Oo0K7bA5BtbUTJJhby+EqcCWBBo
+BM9ryvCiU3gIQdGl2OoDVlBRlvxPfyg0aVR0gechSjaWSavkizcU94jPzgf4upwY
+kd95H8hiHN4jn4TkVjC4sjXhC8f/bf6xwTLbKWy3LZ+htgTNWUc6cnhMoTlRb4HE
+6y1bpZ6Kwpy/MuGW6hKOoA7X166M+NkY1Umse2OqeC3jp03/OYc62/0=
+-----END CERTIFICATE-----
30 spec/config/server-key.pem
@@ -0,0 +1,30 @@
+-----BEGIN RSA PRIVATE KEY-----
+Proc-Type: 4,ENCRYPTED
+DEK-Info: DES-EDE3-CBC,674E2CA3A1EB9681
+
+VAvGcx4EpWS3b9QskR1Z2AV1nMn+AM3nG8yqXPoUR1s0jrQdHbtQx63Nj/rtWLFR
+jx0TK2COqe4ZeRKDt7zpD3jl19xUjDx3GYY6nbpi8tASrtTyjj2gelS1Oxm5c2zV
+o/oLRuhCzdGo3l97DNBXfGd3iR6W7QRD10AmJgSU1umoxhQAi2t8eqSZgLzlmI3L
+Ozx5SDXSf+F46Bju5DWjtZqv7LvlNNd3ssbkuKRr7jL8HlyYhq6RoW7ZPrMw0OD8
+D7GsHQ+mVoXnyw4youi0yHbKYR/kzvmF4rIS3tiPpX3m4PIEVB9YESVXQkffpdyW
+h7Okc4BmvnfGmjWQeU1Z6rYPLPst3jKv/+skmbqMEwEAEJTcJpCgO6aKvfzL5DML
+0rlwmuwzVx5nzdG37Oj6tEusuGcBy0Hy7ErV+v+Dpitb5/vO4EnR4ynwlUzffcW+
+IbtA6sqNMB3+0POdPPnYerpK9QoTQvIpD0cCKClIijzSmmCPjx+NaXPO7JrSPHuO
+1JUn4MTbUqWxm0d9sGuNp/ur4ZPSj3PlYZLSnhR78KxxC8GkIyuAxQPTxz69i6b/
+pQSprQMjznG9bBPDLZU7pVgRLteLnYo+hSyzXJLwBPQBizK/20ZPO7PYZSOwrd7o
++Tde2TlTQURoc1UT3kXqUW35z3vm/dttEYovTkkXAo2rbVCf7BXzZ0L8hXGa+AHA
+abZBJCg6g3NYPX6Wi7OfXXSTo945Kvk7mF1tI0oyo5ydYlJHJ1sMCUAKCSPgec+M
+x6+UT+b6ynK0iq9XrnggFdNg9sJccymSZpfmby8hBqGVCTW5GhKFPAaTFTrVqI+0
+2fQ1mID+3oNXufH7ktAd0hUX4oMy3850dfHBTv4twlRpAQt4WegCcH423fAVQmRv
+6KqvJOYot6TWll0dEhXZd0QtSdAx10P+m6WjMuPfEmztWgLvHlMbLizotvDWRjHq
+Sx+3FQFO7dcmGoYMRfTk47iTvqajwuUKqy4DMvn75iGbBp95pjaPkk0JaWm4XphA
+8hLxEwpi5OmGJEttL/MzIoyfpTYjK+sm928JOZ9in8eV3FedzEpYvevgiUBvOjAC
+BHDbN90HTz7ugMqclNyPgot+7s9BfiNbDa0m1FbSxjb81DBe/XpEmdb5L73gCwAN
+62AyHUrl4a7Af5tVa23+tkSwxeCQUkfvB9qTsIEe5VTjr0sE+j3iLSEpRUDg8Mrk
+DB5ue4VLIsvGhhP1iCcxnzWfNWXpuKQiMoGvnGQKzBAbnuSQ2MpRZEwxZ0+PFVhy
+5wzLPlrXRKP5DSy0MtNfLpKFkZPzygr2tjIZuxZOBCvnvkt4UpDQTKN1YzMx34lP
+S7DET8Cu75VNB6QtJGZZzriiI+fYhGsjz8+UdtXqFVKWJHIxYGWUckcFGfpr/QPT
+zvIb9ZOuvuU1gCcUf1EYDkn2dIaCr/3KC22wQ7Cn+AfenioFxZK/KGofUy1WmJu7
+xuxkpUkcQd5P/gCR2oAfMbMgj2FUcF44OwYEZ/yhyxXHWqk5t4/fxJ6bCZPXdsOt
+y6Rjes+hpDYa/V0l+zErjUKs2/Uw0vh//ppPNofDuyzuqOUTO5JDE4gykZX9eyA2
+-----END RSA PRIVATE KEY-----
84 spec/integration/integration_spec.rb
@@ -0,0 +1,84 @@
+require 'spec_helper'
+
+# add a raw message queue for debugging, might be a good normal feature
+class TestClient < EventMachine::IRC::Client
+ def initialize(options = {})
+ super(options)
+ @received_messages = []
+ end
+
+ def receive_data(data)
+ @received_messages << data
+ super(data)
+ end
+
+ def history
+ "\n" + @received_messages.join("\n") + "\n"
+ end
+end
+
+shared_examples_for "integration" do
+ it 'should work' do
+ dan = client(nick: 'dan')
+ bob = client(nick: 'bob')
+
+ dan.on(:connect) {dan.join('#americano-test')}
+ bob.on(:connect) {bob.join('#americano-test')}
+
+ bob.on(:join) do |who, channel|
+ bob.message(channel, "dan: hello bob")
+ # bob.quit
+ # dan.quit
+ end
+
+ EM.run {
+ dan.connect
+ EM::add_timer(2) {bob.connect}
+ EM::add_timer(5) {EM::stop}
+ }
+
+ # TODO: matchers for commands
+ dan.history.should =~ /Welcome/
+ dan.history.should =~ /JOIN :#americano-test/
+ dan.history.should =~ /dan: hello bob/
+
+ bob.history.should =~ /Welcome/
+ bob.history.should =~ /JOIN :#americano-test/
+ end
+end
+
+# Assumes there is an IRC server running at localhost 6667
+describe EventMachine::IRC::Client, :integration => true do
+ let(:options) do
+ {
+ host: '127.0.0.1',
+ port: '6667'
+ }.merge(@options || {})
+ end
+
+ def client(opts = {})
+ TestClient.new(options.merge(opts))
+ end
+
+ context 'non-ssl' do
+ before do
+ unless `lsof -i :6667 | grep LISTEN`.chomp.size > 1
+ fail "No IRC server running to test integration. See README.md for details"
+ end
+ end
+ it_behaves_like "integration"
+ end
+
+ context 'ssl' do
+ before do
+ @options = {
+ port: '6697',
+ ssl: true
+ }
+ unless `lsof -i :6697 | grep LISTEN`.chomp.size > 1
+ fail "No IRC server running to test integration. See README.md for details"
+ end
+ end
+ it_behaves_like "integration"
+ end
+end
185 spec/lib/em-irc/client_spec.rb
@@ -0,0 +1,185 @@
+require 'spec_helper'
+
+describe EventMachine::IRC::Client do
+ context 'configuration' do
+ it 'defaults host to 127.0.0.1' do
+ subject.host.should == '127.0.0.1'
+ end
+
+ it 'defaults realname to random generated name' do
+ subject.realname.should_not be_blank
+ end
+
+ it 'defaults port to 6667' do
+ subject.port.should == '6667'
+ end
+
+ it 'defaults ssl to false' do
+ subject.ssl.should == false
+ end
+
+ it 'defaults channels to an empty set' do
+ subject.channels.should be_empty
+ end
+
+ it 'should not be connected' do
+ subject.should_not be_connected
+ end
+
+ it 'should default a nick' do
+ subject.nick.should_not be_blank
+ end
+
+ it 'should yield self' do
+ client = described_class.new do |c|
+ c.should be_kind_of(described_class)
+ end
+ end
+
+ it 'should allow a custom logger' do
+ subject.logger = Logger.new(STDOUT)
+ subject.logger = nil
+ end
+ end
+
+ context 'connect' do
+ it 'should create an EM TCP connection with host, port, handler, and self' do
+ EventMachine.should_receive(:connect).with('irc.net', '9999', EventMachine::IRC::Dispatcher, parent: subject)
+ subject.host = 'irc.net'
+ subject.port = '9999'
+ subject.connect
+ end
+
+ it 'should be idempotent' do
+ EventMachine.stub(connect: mock('Connection'))
+ EventMachine.should_receive(:connect).exactly(1).times
+ subject.connect
+ subject.connect
+ end
+ end
+
+ context 'send_data' do
+ before do
+ @connection = mock('Connection')
+ subject.stub(conn: @connection)
+ subject.stub(connected?: true)
+ end
+
+ it 'should return false if not connected' do
+ subject.stub(connected?: nil)
+ subject.send_data("NICK jch").should be_false
+ end
+
+ it 'should send message to irc server' do
+ subject.stub(conn: @connection)
+ @connection.should_receive(:send_data).with("NICK jch\r\n")
+ subject.send_data("NICK jch")
+ end
+ end
+
+ context 'ready' do
+ before do
+ subject.stub(conn: mock.as_null_object)
+ end
+
+ it 'should call :connect callback' do
+ m = mock('callback')
+ m.should_receive(:callback)
+ subject.on(:connect) {m.callback}
+ subject.ready
+ end
+
+ it 'should mark client as connected' do
+ subject.ready
+ subject.should be_connected
+ end
+ end
+
+ context 'unbind' do
+ it 'should call :disconnect callback' do
+ m = mock('callback')
+ m.should_receive(:callback)
+ subject.on(:disconnect) {m.callback}
+ subject.unbind
+ end
+ end
+
+ context 'message parsing' do
+ context 'prefix' do
+ it 'should be optional' do
+ parsed = subject.parse_message('NICK jch')
+ parsed[:prefix].should be_nil
+ end
+
+ it 'should start with :' do
+ parsed = subject.parse_message(':jch!host 123 :params')
+ parsed[:prefix].should == 'jch!host'
+ end
+ end
+
+ context 'params' do
+ it 'should remove leading :' do
+ parsed = subject.parse_message('PING :irc.net')
+ parsed[:params] =~ ['irc.net']
+ end
+ end
+ end
+
+ context 'receive_data' do
+ before do
+ subject.stub(:parse_message).and_return(mock.as_null_object)
+ subject.stub(:handle_parsed_message).and_return(mock.as_null_object)
+ end
+
+ it 'should parse messages separated by \r\n' do
+ data = [
+ ":irc.the.net 001 jessie :Welcome to the Internet Relay Network jessie!~jessie@localhost",
+ ":irc.the.net 002 jessie :Your host is irc.the.net, running version ngircd-17.1 (i386/apple/darwin11.2.0)",
+ ":irc.the.net 003 jessie :This server has been started Fri Feb 03 2012 at 14:42:38 (PST)"
+ ].join("\r\n")
+
+ subject.should_receive(:parse_message).exactly(3).times
+ subject.should_receive(:handle_parsed_message).exactly(3).times
+ subject.receive_data(data)
+ end
+
+ it 'should handle parsed messages' do
+ end
+ end
+
+ context 'handle_parsed_message' do
+ it 'should respond to pings' do
+ subject.should_receive(:pong).with("irc.net")
+ subject.handle_parsed_message({prefix: 'irc.net', command: 'PING', params: ['irc.net']})
+ end
+
+ # TODO: do we want a delegate object and callbacks?
+ # it 'should call optional delegate' do
+ # subject.stub(delegate: mock('Delegate'))
+ # subject.delegate.should_receive(:message)
+ # subject.handle_parsed_message({prefix: 'jessie!jessie@localhost', command: 'PRIVMSG', params: ['#general', 'hello world'])
+ # end
+
+ it 'should trigger :raw callbacks' do
+ m = mock('Parsed Response').as_null_object
+ subject.should_receive(:trigger).with(:raw, m)
+ subject.handle_parsed_message(m)
+ end
+ end
+
+ context 'callbacks' do
+ it 'should register multiple' do
+ m = mock('Callback')
+ subject.on(:foo) {m.callback}
+ subject.on(:foo) {m.callback}
+ subject.callbacks[:foo].size.should == 2
+ end
+
+ it 'should trigger with params' do
+ m = mock('Callback')
+ m.should_receive(:callback).with('arg')
+ subject.on(:foo) {|arg| m.callback(arg)}
+ subject.trigger(:foo, 'arg')
+ end
+ end
+end
44 spec/lib/em-irc/dispatcher_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+
+shared_examples_for 'dispatcher' do
+ it 'should delegate connection methods to parent' do
+ parent = Class.new do
+ attr_accessor :conn
+ def ssl
+ $ssl
+ end
+
+ def receive_data(data)
+ EventMachine::stop_event_loop
+ raise "Didn't get expected message" unless data == 'message'
+ end
+ end.new
+ parent.should_receive(:ready)
+ parent.should_receive(:unbind)
+ EventMachine.run {
+ EventMachine::start_server('127.0.0.1', '198511', described_class, parent: parent)
+ EventMachine::connect('127.0.0.1', '198511') do |conn|
+ conn.send_data("message")
+ end
+ EventMachine::add_timer(2) {
+ EventMachine::stop_event_loop
+ raise "Never reached receive_data or took too long"
+ }
+ }
+ end
+end
+
+describe EventMachine::IRC::Dispatcher do
+ context 'ssl integration' do
+ before {$ssl = true}
+ it 'behaves like dispatcher' do
+ pending "not sure how to test ssl"
+ end
+ # it_behaves_like "dispatcher"
+ end
+
+ context 'non-ssl integration' do
+ before {$ssl = false}
+ it_behaves_like "dispatcher"
+ end
+end
6 spec/spec_helper.rb
@@ -0,0 +1,6 @@
+require File.expand_path '../lib/em-irc', File.dirname(__FILE__)
+Bundler.require :test
+require 'logger'
+
+RSpec.configure do |c|
+end
Please sign in to comment.
Something went wrong with that request. Please try again.