Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Code ported across from capistrano-fanfare.

  • Loading branch information...
commit afb61973c8f5040b7b71bea21938f99866efc161 1 parent 27bab26
@fnichol authored
View
2  .gitignore
@@ -15,3 +15,5 @@ spec/reports
test/tmp
test/version_tmp
tmp
+.rvmrc
+.rbx/
View
11 .travis.yml
@@ -0,0 +1,11 @@
+language: ruby
+rvm:
+ - 1.9.3
+ - 1.9.2
+ - rbx-19mode
+ - 1.8.7
+ - ree
+ - rbx-18mode
+ - jruby-18mode
+ - ruby-head
+ - jruby-head
View
12 Gemfile
@@ -2,3 +2,15 @@ source 'https://rubygems.org'
# Specify your gem's dependencies in campy.gemspec
gemspec
+
+group :test do
+ gem 'rake', '~> 0.9'
+
+ gem 'growl'
+ gem 'guard'
+ gem 'guard-minitest'
+end
+
+platforms :jruby do
+ gem 'jruby-openssl'
+end
View
5 Guardfile
@@ -0,0 +1,5 @@
+guard 'minitest' do
+ # with Minitest::Spec
+ watch(%r|^spec/(.*)_spec\.rb|)
+ watch(%r|^lib/(.*)([^/]+)\.rb|) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" }
+end
View
10 Rakefile
@@ -1,2 +1,12 @@
#!/usr/bin/env rake
require "bundler/gem_tasks"
+
+require 'rake/testtask'
+
+Rake::TestTask.new do |t|
+ t.libs.push "lib"
+ t.test_files = FileList['spec/**/*_spec.rb']
+ t.verbose = true
+end
+
+task :default => 'test'
View
38 bin/campy
@@ -0,0 +1,38 @@
+#!/usr/bin/env ruby
+
+$:.unshift File.join(File.dirname(__FILE__), '..', 'lib')
+require 'campy'
+require 'yaml'
+
+def options
+ yaml_file = File.expand_path(ENV['CAMPFIRE_YAML_FILE'] || '~/.campfire.yml')
+ if !File.exists?(yaml_file)
+ abort "File '#{yaml_file}' does not exist with campfire configuration."
+ end
+ YAML.load_file(yaml_file)
+end
+
+def usage
+ <<-USAGE.gsub(/^ {4}/, '')
+
+ Usage
+
+ campy <action> <message>
+
+ Actions
+
+ help - Display CLI help (this output)
+ speak - Speak a message into the campfire room
+ play - Play a sound into the campfire room
+
+ USAGE
+end
+
+if ARGV.first == "help"
+ puts usage
+ exit
+end
+
+abort usage if ARGV.empty? || !%w{speak play}.include?(ARGV.first)
+
+Campy::Room.new(options).send(ARGV.shift, ARGV.shift)
View
5 campy.gemspec
@@ -14,4 +14,9 @@ Gem::Specification.new do |gem|
gem.name = "campy"
gem.require_paths = ["lib"]
gem.version = Campy::VERSION
+
+ gem.add_dependency "multi_json", "~> 1.0"
+
+ gem.add_development_dependency "minitest", "~> 2.12.0"
+ gem.add_development_dependency "webmock", "~> 1.8.5"
end
View
3  lib/campy.rb
@@ -1,4 +1,5 @@
-require "campy/version"
+require 'campy/version'
+require 'campy/room'
module Campy
# Your code goes here...
View
186 lib/campy/room.rb
@@ -0,0 +1,186 @@
+# -*- encoding: utf-8 -*-
+require 'net/https'
+require 'multi_json'
+
+module Campy
+ class Room
+ # Public: Error class raised when a room ID cannot be found with a
+ # given name.
+ class NotFound < RuntimeError ; end
+
+ # Public: Error class raised when an HTTP error has occured.
+ class ConnectionError < RuntimeError ; end
+
+ # Public: Returns the String account name.
+ attr_reader :account
+
+ # Public: Returns the String room name.
+ attr_reader :room
+
+ # Public: Returns the API token String for a user.
+ attr_reader :ssl
+
+ # Public: Initializes a Room from a Hash of configuration options.
+ #
+ # options - A Hash of options to set up the Room (default: {}):
+ # :account - The String account/subdomain name.
+ # :room - The String room name, not the room ID.
+ # :token - The API token String for a user.
+ # :ssl - A truthy object which is true when SSL is
+ # required (default: true).
+ def initialize(options = {})
+ options = { :ssl => true }.merge(options)
+
+ [:account, :room, :token, :ssl].each do |option|
+ instance_variable_set "@#{option}", options[option]
+ end
+ end
+
+ # Public: Returns the Integer room ID of the Campfire room.
+ #
+ # Returns the Integer room ID.
+ # Raises NotFound if a room cannot be found for the given name.
+ # Raises ConnectionError if an HTTP error occurs.
+ def room_id
+ @room_id ||= fetch_room_id
+ end
+
+ # Public: Posts a message into the campfire room.
+ #
+ # msg - A String message.
+ #
+ # Returns true if message is delivered.
+ # Raises ConnectionError if an HTTP error occurs.
+ def speak(msg)
+ send_message(msg)
+ end
+
+ # Public: Plays a sound into the campfire room.
+ #
+ # sound - A String representing the sound.
+ #
+ # Returns true if message is delivered.
+ # Raises ConnectionError if an HTTP error occurs.
+ def play(msg)
+ send_message(msg, 'SoundMessage')
+ end
+
+ private
+
+ # Internal: Array of errors that will be wrapped when using Net::HTTP.
+ HTTP_ERRORS = [Timeout::Error, Errno::EINVAL, Errno::ECONNRESET,
+ EOFError, Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError,
+ Net::ProtocolError, SocketError, OpenSSL::SSL::SSLError,
+ Errno::ECONNREFUSED]
+
+ # Internal: Returns the API token String for a user.
+ attr_reader :token
+
+ # Internal: Returns the campfire hostname with subdomain.
+ def host
+ "#{account}.campfirenow.com"
+ end
+
+ # Internal: Returns the Integer HTTP port number to connect with.
+ def port
+ ssl ? 443 : 80
+ end
+
+ # Internal: Returns the Integer number of the room.
+ #
+ # Returns the Integer room number.
+ # Raises NotFound if a room cannot be found for the given name.
+ # Raises ConnectionError if an HTTP error occurs.
+ def fetch_room_id
+ connect do |http|
+ response = http.request(http_request(:get, "/rooms.json"))
+
+ case response
+ when Net::HTTPOK
+ find_room_in_json(MultiJson.decode(response.body))
+ else
+ raise ConnectionError
+ end
+ end
+ end
+
+ # Internal: Posts a message to the campfire room.
+ #
+ # msg - The String message to send.
+ # type - The String type of campfire message (default: TextMessage).
+ #
+ # Returns true if message is delivered.
+ # Raises ConnectionError if an HTTP error occurs.
+ def send_message(msg, type = 'TextMessage')
+ connect do |http|
+ request = http_request(:post, "/room/#{room_id}/speak.json")
+ request.body = MultiJson.encode(
+ { :message => { :body => msg, :type => type } })
+ response = http.request(request)
+
+ case response
+ when Net::HTTPCreated
+ true
+ else
+ raise ConnectionError,
+ "Error sending message '#{msg}' (#{response.class})"
+ end
+ end
+ end
+
+ # Internal: Parses through the rooms JSON response and returns the
+ # Integer room ID.
+ #
+ # json - the rooms Hash of JSON data.
+ #
+ # Returns the Integer room number.
+ # Raises NotFound if a room cannot be found for the given name.
+ def find_room_in_json(json)
+ room_hash = json["rooms"].find { |r| r["name"] == room }
+
+ if room_hash
+ room_hash["id"]
+ else
+ raise NotFound, "Room name '#{room}' could not be found."
+ end
+ end
+
+ # Internal: Creates a Net::HTTP connection and yields to a block with
+ # the connection.
+ #
+ # Yields the Net::HTTP connection.
+ #
+ # Returns the return value (if any) of the block.
+ # Raises ConnectionError if any common HTTP errors are raised.
+ def connect
+ http = Net::HTTP.new(host, port)
+ http.use_ssl = ssl
+
+ begin
+ yield http
+ rescue *HTTP_ERRORS => exception
+ raise ConnectionError, "#{exception.class.name}: #{exception.message}"
+ end
+ end
+
+ # Internal: Returns a Net::HTTPRequest object initialized with
+ # authentication and content headers set.
+ #
+ # verb - A Symbol representing an HTTP verb.
+ # path - The String path of the request.
+ #
+ # Examples
+ #
+ # http_request(:get, "/rooms.json")
+ # http_request(:post, "/room/1/speak.json")
+ #
+ # Returns a Net::HTTPRequest object.
+ def http_request(verb, path)
+ klass = klass = Net::HTTP.const_get(verb.to_s.capitalize)
+ request = klass.new(path)
+ request.basic_auth(token, "X")
+ request["Content-Type"] = "application/json"
+ request
+ end
+ end
+end
View
1  lib/campy/version.rb
@@ -1,3 +1,4 @@
+# -*- encoding: utf-8 -*-
module Campy
VERSION = "0.0.1"
end
View
166 spec/campy/room_spec.rb
@@ -0,0 +1,166 @@
+# -*- encoding: utf-8 -*-
+require 'minitest/autorun'
+require 'webmock/minitest'
+require 'campy/room'
+
+describe Campy::Room do
+ WRAPPED_ERRORS = [Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, EOFError,
+ Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError,
+ SocketError, OpenSSL::SSL::SSLError, Errno::ECONNREFUSED]
+
+ let(:opts) do
+ { :account => 'zubzub', :token => 'yepyep',
+ :room => 'myroom', :ssl => true }
+ end
+
+ let(:stub_rooms!) do
+ stub_request(:get, "https://yepyep:X@zubzub.campfirenow.com/rooms.json").
+ with(:headers => {'Content-Type' => 'application/json'}).
+ to_return(:status => 200, :body => fixture("rooms"), :headers => {})
+ end
+
+ let(:stub_rooms_no_room!) do
+ stub_request(:get, "https://yepyep:X@zubzub.campfirenow.com/rooms.json").
+ with(:headers => {'Content-Type' => 'application/json'}).
+ to_return(:status => 200, :body => fixture("no_rooms"), :headers => {})
+ end
+
+ let(:stub_rooms_invalid_token!) do
+ stub_request(:get, "https://yepyep:X@zubzub.campfirenow.com/rooms.json").
+ with(:headers => {'Content-Type' => 'application/json'}).
+ to_return(:status => 401, :body => "HTTP Basic: Access denied.\n",
+ :headers => {})
+ end
+
+ def stub_rooms_error!(error)
+ stub_request(:get, "https://yepyep:X@zubzub.campfirenow.com/rooms.json").
+ with(:headers => {'Content-Type' => 'application/json'}).
+ to_raise(error)
+ end
+
+ def stub_speak!(msg, type = 'TextMessage')
+ stub_request(:post, "https://yepyep:X@zubzub.campfirenow.com/room/123456/speak.json").
+ with(:headers => {'Content-Type' => 'application/json'},
+ :body => {:message => {:body => msg, :type => type}}).
+ to_return(:status => 201, :headers => {},
+ :body => fixture("speak").sub(/@@MESSAGE@@/, msg))
+ end
+
+ def stub_speak_error!(error)
+ stub_request(:post, "https://yepyep:X@zubzub.campfirenow.com/room/123456/speak.json").
+ with(:headers => {'Content-Type' => 'application/json'}).
+ to_raise(error)
+ end
+
+ def fixture(name)
+ File.read(File.dirname(__FILE__) + "/../fixtures/webmock_#{name}.txt")
+ end
+
+ describe "#initialize" do
+ it "takes a hash of campfire configuration" do
+ room = Campy::Room.new(opts)
+ room.account.must_equal 'zubzub'
+ room.room.must_equal 'myroom'
+ end
+
+ it "defaults to SSL mode enabled" do
+ opts.delete(:ssl)
+ room = Campy::Room.new(opts)
+ room.ssl.must_equal true
+ end
+ end
+
+ describe "#room_id" do
+ let(:subject) { Campy::Room.new(opts) }
+
+ it "fetches the room id from the API" do
+ stub_rooms!
+ subject.room_id.must_equal 666666
+ end
+
+ it "raises NotFound if no room is found" do
+ stub_rooms_no_room!
+
+ proc { subject.room_id }.must_raise(
+ Campy::Room::NotFound)
+ end
+
+ it "raises ConnectionError if the token is invalid" do
+ stub_rooms_invalid_token!
+
+ proc { subject.room_id }.must_raise(
+ Campy::Room::ConnectionError)
+ end
+
+ WRAPPED_ERRORS.each do |error|
+ it "wraps #{error} and raises a ConnectionError" do
+ stub_rooms_error!(error)
+
+ proc { subject.room_id }.must_raise(
+ Campy::Room::ConnectionError)
+ end
+ end
+ end
+
+ describe "#speak" do
+ let(:subject) { Campy::Room.new(opts) }
+
+ before do
+ # stub out #room_id since we don't care about this API call
+ def subject.room_id ; 123456 ; end
+ end
+
+ it "calls the speak API with a message" do
+ stub = stub_speak!("talking about talking")
+ subject.speak "talking about talking"
+
+ assert_requested(stub)
+ end
+
+ it "returns true when message is delivered" do
+ stub = stub_speak!("talking about talking")
+
+ subject.speak("talking about talking").must_equal true
+ end
+
+ WRAPPED_ERRORS.each do |error|
+ it "wraps #{error} and raises a ConnectionError" do
+ stub_speak_error!(error)
+
+ proc { subject.speak "nope" }.must_raise(
+ Campy::Room::ConnectionError)
+ end
+ end
+ end
+
+ describe "#play" do
+ let(:subject) { Campy::Room.new(opts) }
+
+ before do
+ # stub out #room_id since we don't care about this API call
+ def subject.room_id ; 123456 ; end
+ end
+
+ it "calls the play API with a sound" do
+ stub = stub_speak!("tada", "SoundMessage")
+ subject.play "tada"
+
+ assert_requested(stub)
+ end
+
+ it "returns true when message is delivered" do
+ stub = stub_speak!("tada", "SoundMessage")
+
+ subject.play("tada").must_equal true
+ end
+
+ WRAPPED_ERRORS.each do |error|
+ it "wraps #{error} and raises a ConnectionError" do
+ stub_speak_error!(error)
+
+ proc { subject.play "tada" }.must_raise(
+ Campy::Room::ConnectionError)
+ end
+ end
+ end
+end
View
4 spec/fixtures/webmock_no_rooms.txt
@@ -0,0 +1,4 @@
+{
+ "rooms": [
+ ]
+}
View
22 spec/fixtures/webmock_rooms.txt
@@ -0,0 +1,22 @@
+{
+ "rooms": [
+ {
+ "created_at": "2012/01/01 00:01:02 +0000",
+ "id": 123456,
+ "locked": false,
+ "membership_limit": 4,
+ "name": "nuggettalk",
+ "topic": "Pop go the nuggets",
+ "updated_at": "2012/02/23 02:21:49 +0000"
+ },
+ {
+ "created_at": "2011/01/01 00:02:03 +0000",
+ "id": 666666,
+ "locked": false,
+ "membership_limit": 12,
+ "name": "myroom",
+ "topic": "There is only one test",
+ "updated_at": "2011/12/05 16:23:40 +0000"
+ }
+ ]
+}
View
11 spec/fixtures/webmock_speak.txt
@@ -0,0 +1,11 @@
+{
+ "message": {
+ "body": "@@MESSAGE@@",
+ "created_at": "2012/04/04 18:11:36 +0000",
+ "id": 999999999,
+ "room_id": 123456,
+ "starred": false,
+ "type": "TextMessage",
+ "user_id": 666
+ }
+}
Please sign in to comment.
Something went wrong with that request. Please try again.