Permalink
Browse files

Code ported across from capistrano-fanfare.

  • Loading branch information...
1 parent 27bab26 commit afb61973c8f5040b7b71bea21938f99866efc161 @fnichol committed Apr 4, 2012
Showing with 475 additions and 1 deletion.
  1. +2 −0 .gitignore
  2. +11 −0 .travis.yml
  3. +12 −0 Gemfile
  4. +5 −0 Guardfile
  5. +10 −0 Rakefile
  6. +38 −0 bin/campy
  7. +5 −0 campy.gemspec
  8. +2 −1 lib/campy.rb
  9. +186 −0 lib/campy/room.rb
  10. +1 −0 lib/campy/version.rb
  11. +166 −0 spec/campy/room_spec.rb
  12. +4 −0 spec/fixtures/webmock_no_rooms.txt
  13. +22 −0 spec/fixtures/webmock_rooms.txt
  14. +11 −0 spec/fixtures/webmock_speak.txt
View
@@ -15,3 +15,5 @@ spec/reports
test/tmp
test/version_tmp
tmp
+.rvmrc
+.rbx/
View
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -1,4 +1,5 @@
-require "campy/version"
+require 'campy/version'
+require 'campy/room'
module Campy
# Your code goes here...
View
@@ -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,3 +1,4 @@
+# -*- encoding: utf-8 -*-
module Campy
VERSION = "0.0.1"
end
Oops, something went wrong.

0 comments on commit afb6197

Please sign in to comment.