Skip to content
Browse files

rev 0

  • Loading branch information...
0 parents commit b1e750b5102f1cadee7a7d9ef74ffee666a684f8 @jamesmacaulay committed
8 .gitignore
@@ -0,0 +1,8 @@
+.DS_Store
+
+*.orig
+
+.dotest
+
+config.yml
+last_updated_at
20 MIT-LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2008 James MacAulay
+
+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 PURPOa AND
+NONINFRINGEMENT. IN NO EVENT SaALL 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.
14 README.markdown
@@ -0,0 +1,14 @@
+ backpack + campfire =
+
+Backfire
+========
+
+1. Copy config.example.yml to config.yml and edit to taste.
+2. `ruby backfire.rb`
+3. Bask in the glory of continually updated journals + statuses.
+
+[James MacAulay][] is the developer and can be contacted at <jmacaulay@gmail.com>.
+
+Unless otherwise noted, all files in this project are licensed under the MIT license (supplied in `MIT-LICENSE`).
+
+[James MacAulay]:http://jmacaulay.net
143 backfire.rb
@@ -0,0 +1,143 @@
+require 'lib/tinder/lib/tinder'
+require 'lib/backpack_api_wrapper'
+require 'rubygems'
+require 'active_support'
+
+module Backfire
+ LAST_UPDATED_AT_FILE='last_updated_at'
+ CONFIG_FILE='config.yml'
+ @@last_updated_at = nil
+
+ def self.config
+ @@config ||= YAML.load(File.read(CONFIG_FILE))
+ end
+
+ def self.backpack
+ @@backpack ||= Backpack.new(config['backpack']['subdomain'], config['backpack']['token'], config['backpack']['ssl'])
+ end
+
+ def self.campfire
+ @@campfire ||= begin
+ campfire = Tinder::Campfire.new(config['campfire']['subdomain'], :ssl => config['campfire']['ssl'])
+ campfire.login(config['campfire']['login'], config['campfire']['password'])
+ campfire
+ end
+ end
+
+ def self.room(id = nil)
+ id ||= config['campfire']
+ Tinder::Room.new(campfire, 100257)
+ end
+
+ def self.last_updated_at
+ @@last_updated_at ||= begin
+ str = File.read(LAST_UPDATED_AT_FILE) rescue nil
+ (str.blank? ? nil : Time.parse(str))
+ end
+ end
+
+ def self.is_now_updated
+ @@last_updated_at = Time.now
+ File.open(LAST_UPDATED_AT_FILE, 'w+') do |file|
+ file.write(@@last_updated_at.to_s)
+ end
+ true
+ rescue
+ false
+ end
+
+ def self.was_never_updated
+ File.open(LAST_UPDATED_AT_FILE, 'w+') do |file|
+ file.write('')
+ end
+ @@last_updated_at = nil
+ true
+ rescue
+ false
+ end
+
+ def self.update_campfire
+ puts "*** update_campfire"
+ statuses = Status.all
+ entries = JournalEntry.new_entries
+ unless entries.empty? && self.last_updated_at && !(statuses.find {|s| s.updated_at >= self.last_updated_at})
+ update = ''
+ (entries + statuses).group_by(&:user).each do |user_array|
+ user = user_array.first
+ user_statuses, user_entries = user_array.last.partition {|item| item.is_a? Status}
+ status = user_statuses.first
+ user_entries = user_entries.sort {|a,b| b.updated_at <=> a.updated_at }
+ unless user_entries.empty? and (self.last_updated_at ? status.updated_at < self.last_updated_at : true)
+ update << "\n#{user.name}: #{status.message unless user_statuses.empty?}\n"
+ user_entries.each do |entry|
+ update << " * #{entry.body}\n"
+ end
+ end
+ end
+ room.paste(update) unless config['campfire']['test_mode'] == true
+ puts "*** Pasted to campfire:\n"
+ puts update
+ end
+ self.is_now_updated
+ end
+
+ def self.go(interval = 20)# seconds
+ while true
+ update_campfire
+ sleep interval
+ end
+ end
+
+ class BackpackUser
+ attr_reader :name, :id
+ def initialize(ary)
+ hash = Array(ary).first
+ @name = hash['name'].first
+ @id = hash['id'].first['content'].to_i
+ end
+
+ def ==(obj)
+ return false unless obj.class == self.class
+ return (obj.name == self.name) && (obj.id == self.id)
+ end
+ end
+
+ class JournalEntry
+ attr_reader :user, :body, :id, :created_at, :updated_at
+
+ def initialize(hash)
+ @user = BackpackUser.new(hash['user'])
+ @id = hash['id'].first['content'].to_i
+ @updated_at = Time.parse(hash['updated-at'].first['content'])
+ @created_at = Time.parse(hash['created-at'].first['content'])
+ @body = hash['body'].first
+ end
+
+ def self.new_entries
+ entries = Backfire.backpack.list_journal_entries['journal-entry']
+ if Backfire.last_updated_at
+ entries = entries.reject {|e| Time.parse(e['updated-at'].first['content']) < Backfire.last_updated_at}
+ end
+ entries.map {|e| self.new(e)}
+ end
+ end
+
+ class Status
+ attr_reader :user, :message, :id, :created_at, :updated_at
+
+ def initialize(hash)
+ @user = BackpackUser.new(hash['user'])
+ @id = hash['id'].first['content'].to_i
+ @updated_at = Time.parse(hash['updated-at'].first['content'])
+ @message = hash['message'].first
+ end
+
+ def self.all
+ Backfire.backpack.list_statuses['status'].map {|status| self.new(status)}
+ end
+ end
+
+
+end # module Backfire
+
+Backfire.go(Backfire.config['global']['interval'])
16 config.example.yml
@@ -0,0 +1,16 @@
+global:
+ interval: 20
+
+campfire:
+ subdomain: 'business-time'
+ login: 'you@email.com'
+ password: 'secret'
+ default_room: 1234
+ test_mode: false
+ ssl: true
+
+backpack:
+ subdomain: 'business-time'
+ token: '0123456789abcdef'
+ ssl: true
+
226 lib/backpack_api_wrapper.rb
@@ -0,0 +1,226 @@
+# Requires that XmlSimple is already loaded.
+# Author: David Heinemeier Hansson, 37signals
+# Original obtained from http://developer.37signals.com/backpack/ruby_wrapper.rb
+# Backpack API docs at http://developer.37signals.com/backpack/index.shtml
+# Updated by James MacAulay for Backfire
+
+require 'yaml'
+require 'net/https'
+
+class Backpack
+ attr_accessor :username, :token, :current_page_id
+
+ def initialize(username, token, use_ssl = false)
+ @username, @token = username, token
+ connect
+ end
+
+ def connect(use_ssl = false)
+ @connection = Net::HTTP.new("#{@username}.backpackit.com", use_ssl ? 443 : 80)
+ @connection.use_ssl = use_ssl
+ @connection.verify_mode = OpenSSL::SSL::VERIFY_NONE if use_ssl
+ end
+
+ def page_id=(id)
+ self.current_page_id = id
+ end
+
+ def request(path, parameters = {}, second_try = false)
+ parameters = { "token" => @token }.merge(parameters)
+
+ response = @connection.post("/ws/#{path}", parameters.to_yaml, "X-POST_DATA_FORMAT" => "yaml")
+
+ if response.code == "200"
+ result = XmlSimple.xml_in(response.body)
+ result.delete "success"
+ result.empty? ? true : result
+ elsif response.code == "302" && !second_try
+ connect(true)
+ request(path, parameters, true)
+ else
+ raise "Error occured (#{response.code}): #{response.body}"
+ end
+ end
+
+
+ # Items ----
+
+ def list_items(page_id = current_page_id)
+ request "page/#{page_id}/items/list"
+ end
+
+ def create_item(content, page_id = current_page_id)
+ request "page/#{page_id}/items/add", "item" => { "content" => content }
+ end
+
+ def update_item(item_id, content, page_id = current_page_id)
+ request "page/#{page_id}/items/update/#{item_id}", "item" => { "content" => content }
+ end
+
+ def destroy_item(item_id, page_id = current_page_id)
+ request "page/#{page_id}/items/destroy/#{item_id}"
+ end
+
+ def toggle_item(item_id, page_id = current_page_id)
+ request "page/#{page_id}/items/toggle/#{item_id}"
+ end
+
+ def move_item(item_id, direction, page_id = current_page_id)
+ request "page/#{page_id}/items/move/#{item_id}", "direction" => "move_#{direction}"
+ end
+
+
+ # Notes ----
+
+ def list_notes(page_id = current_page_id)
+ request "page/#{page_id}/notes/list"
+ end
+
+ def create_note(title, body, page_id = current_page_id)
+ request "page/#{page_id}/notes/create", "note" => { "title" => title, "body" => body }
+ end
+
+ def update_note(note_id, title, body, page_id = current_page_id)
+ request "page/#{page_id}/notes/update/#{note_id}", "note" => { "title" => title, "body" => body }
+ end
+
+ def destroy_note(note_id, page_id = current_page_id)
+ request "page/#{page_id}/notes/destroy/#{note_id}"
+ end
+
+
+ # Separators (dividers) ----
+
+ def create_separator(name, page_id = current_page_id)
+ request "page/#{page_id}/separators/create", "separator" => { "name" => name }
+ end
+
+ def update_separator(separator_id, name, page_id = current_page_id)
+ request "page/#{page_id}/separators/update/#{separator_id}", "separator" => { "name" => name }
+ end
+
+ def destroy_separator(separator_id, page_id = current_page_id)
+ request "page/#{page_id}/separators/destroy/#{separator_id}"
+ end
+
+
+ # Emails ----
+
+ def list_emails(page_id = current_page_id)
+ request "page/#{page_id}/emails/list"
+ end
+
+ def show_email(email_id, page_id = current_page_id)
+ request "page/#{page_id}/emails/show/#{email_id}"
+ end
+
+ def destroy_email(email_id, page_id = current_page_id)
+ request "page/#{page_id}/emails/destroy/#{email_id}"
+ end
+
+
+ # Tags ----
+
+ def list_pages_with_tag(tag_id)
+ request "tags/select/#{tag_id}"
+ end
+
+ def tag_page(tags, page_id = current_page_id)
+ request "page/#{page_id}/tags/tag", "tags" => tags
+ end
+
+
+ # Pages ----
+
+ def list_pages
+ request "pages/all"
+ end
+
+ def create_page(title, body)
+ request "pages/new", "page" => { "title" => title, "description" => body }
+ end
+
+ def show_page(page_id = current_page_id)
+ request "page/#{page_id}"
+ end
+
+ def reorder_page(belonging_ids, page_id = current_page_id)
+ belonging_ids *= ' ' unless belonging_ids.is_a?(String)
+ request "page/#{page_id}/destroy", 'belongings' => belonging_ids
+ end
+
+ def destroy_page(page_id = current_page_id)
+ request "page/#{page_id}/destroy"
+ end
+
+ def update_title(title, page_id = current_page_id)
+ request "page/#{page_id}/update_title", "page" => { "title" => title }
+ end
+
+ def update_body(body, page_id = current_page_id)
+ request "page/#{page_id}/update_body", "page" => { "description" => body }
+ end
+
+ def link_page(linked_page_id, page_id = current_page_id)
+ request "page/#{page_id}/link", "linked_page_id" => linked_page_id
+ end
+
+ def unlink_page(linked_page_id, page_id = current_page_id)
+ request "page/#{page_id}/unlink", "linked_page_id" => linked_page_id
+ end
+
+ def share_page(email_addresses, public_page = nil, page_id = current_page_id)
+ parameters = { "email_addresses" => email_addresses }
+ parameters = parameters.merge({ "page" => { "public" => public_page ? "1" : "0" }}) unless public_page.nil?
+ request "page/#{page_id}/share", parameters
+ end
+
+
+ # Journal Entries ---
+
+ def list_journal_entries(count = 100,page = 0)
+ request "journal_entries", 'count' => count, 'n' => page
+ end
+
+ def list_journal_entries_from_user(user_id)
+ request "users/#{user_id}/journal_entries"
+ end
+
+ def show_journal_entry(id)
+ request "journal_entries/#{id}"
+ end
+
+
+
+ # Statuses ---
+
+ def list_statuses
+ request "statuses"
+ end
+
+ def show_status_of_user(user_id)
+ request "users/#{user_id}/status"
+ end
+
+ def update_status_of_user(user_id, message)
+ request "users/#{user_id}/status", 'status' => {'message' => message}
+ end
+
+ # Reminders ---
+
+ def list_reminders
+ request "reminders"
+ end
+
+ def create_reminder(content, remind_at = "")
+ request "reminders/create", "reminder" => { "content" => content, "remind_at" => remind_at }
+ end
+
+ def update_reminder(reminder_id, content, remind_at)
+ request "reminders/update/#{reminder_id}", "reminder" => { "content" => content, "remind_at" => remind_at }
+ end
+
+ def destroy_reminder(reminder_id)
+ request "reminders/destroy/#{reminder_id}"
+ end
+end
30 lib/tinder/CHANGELOG.txt
@@ -0,0 +1,30 @@
+0.1.7 - unreleased
+* Don't join the room when only speaking [Brian Donovan]
+* Added support for HTTP proxies
+* Fix listening for messages that contain URLs [Jared Kuolt]
+
+0.1.6 - 2008-03-07
+* Added Room#topic for getting the current topic [Even Weaver]
+* Trap INT in #listen(&block) [borrowed from Chris Shea's Pyre]
+
+0.1.5 - 2008-01-25
+* Fixed Room#listen, which was broken by latest Campfire deploy
+* Fixed timeout when listening but not speaking that will eventually log you out [Clinton R. Nixon]
+
+0.1.4 - 2007-07-23
+* Support for transcripts
+* Fixed Room#leave, which was broken by a Campfire deployment [Andy Smith]
+
+0.1.3 - 2007-02-12
+* added ssl support [Tero Parviainen]
+
+0.1.2 - 2007-01-27
+* fixed bug preventing #listen from working without a block
+
+0.1.1 - 2007-01-27
+* fix bug preventing speak from working
+* incorporated "watching" from http://soylentfoo.jnewland.com/articles/2006/12/07/updates-to-marshmallow-the-campfire-bot
+
+0.1.0 - 2007-01-23
+* Initial release as gem
+* Get the users in a room [Tero Parviainen]
9 lib/tinder/Manifest.txt
@@ -0,0 +1,9 @@
+CHANGELOG.txt
+Manifest.txt
+README.txt
+Rakefile
+init.rb
+lib/tinder.rb
+lib/tinder/campfire.rb
+lib/tinder/room.rb
+lib/tinder/version.rb
40 lib/tinder/README.txt
@@ -0,0 +1,40 @@
+= Tinder - get the Campfire started
+
+Tinder is a library for interfacing with Campfire, the chat application from 37Signals. Unlike Marshmallow, it is designed to be a full-featured API (since 37Signals doesn't provide a real one), allowing you to programatically manage and speak/listen in chat rooms.
+
+== Usage
+
+ campfire = Campfire.new 'mysubdomain'
+ campfire.login 'myemail@example.com', 'mypassword'
+ room = campfire.create_room 'New Room', 'My new campfire room to test tinder'
+ room.rename 'New Room Name'
+ room.speak 'Hello world!'
+ room.paste "my pasted\ncode"
+ room.destroy
+
+ See the RDoc for more details.
+
+== Requirements
+
+* Active Support
+ gem install activesupport
+* Hpricot
+ gem install hpricot
+
+== Installation
+
+Tinder can be installed as a gem or a Rails plugin:
+
+ gem install tinder
+
+ script/plugin install http://source.collectiveidea.com/public/tinder/trunk
+
+== Development
+
+The source for Tinder is available at http://source.collectiveidea.com/public/tinder/trunk. Development can be followed at http://opensoul.org/tags/tinder. Contributions are welcome!
+
+== ToDo
+
+* Tests! (unit and remote)
+* Log in via guest url
+* Marshmallow-style integration scripts for exception notification and continuous integration
24 lib/tinder/Rakefile
@@ -0,0 +1,24 @@
+require 'rubygems'
+require 'hoe'
+require File.join(File.dirname(__FILE__), 'lib', 'tinder', 'version')
+
+# RDOC_OPTS = ['--quiet', '--title', "Tinder",
+# "--opname", "index.html",
+# "--line-numbers",
+# "--main", "README",
+# "--inline-source"]
+#
+# Generate all the Rake tasks
+
+hoe = Hoe.new('tinder', ENV['VERSION'] || Tinder::VERSION::STRING) do |p|
+ p.rubyforge_name = 'tinder'
+ p.summary = "An (unofficial) Campfire API"
+ p.description = "An API for interfacing with Campfire, the 37Signals chat application."
+ p.author = 'Brandon Keepers'
+ p.email = 'brandon@opensoul.org'
+ p.url = 'http://tinder.rubyforge.org'
+ p.test_globs = ["test/**/*_test.rb"]
+ p.changes = p.paragraphs_of('CHANGELOG.txt', 0..1).join("\n\n")
+ p.extra_deps << ['activesupport']
+ p.extra_deps << ['hpricot']
+end
1 lib/tinder/init.rb
@@ -0,0 +1 @@
+require 'tinder'
14 lib/tinder/lib/tinder.rb
@@ -0,0 +1,14 @@
+require 'rubygems'
+require 'active_support'
+require 'uri'
+require 'net/http'
+require 'net/https'
+require 'open-uri'
+require 'hpricot'
+
+Dir[File.join(File.dirname(__FILE__), 'tinder/**/*.rb')].sort.each { |lib| require lib }
+
+module Tinder
+ class Error < StandardError; end
+ class SSLRequiredError < Error; end
+end
182 lib/tinder/lib/tinder/campfire.rb
@@ -0,0 +1,182 @@
+module Tinder
+
+ # == Usage
+ #
+ # campfire = Tinder::Campfire.new 'mysubdomain'
+ # campfire.login 'myemail@example.com', 'mypassword'
+ # room = campfire.create_room 'New Room', 'My new campfire room to test tinder'
+ # room.speak 'Hello world!'
+ # room.destroy
+ class Campfire
+ attr_reader :subdomain, :uri
+
+ # Create a new connection to the campfire account with the given +subdomain+.
+ #
+ # == Options:
+ # * +:ssl+: use SSL for the connection, which is required if you have a Campfire SSL account.
+ # Defaults to false
+ # * +:proxy+: a proxy URI. (e.g. :proxy => 'http://user:pass@example.com:8000')
+ #
+ # c = Tinder::Campfire.new("mysubdomain", :ssl => true)
+ def initialize(subdomain, options = {})
+ options = { :ssl => false }.merge(options)
+ @cookie = nil
+ @subdomain = subdomain
+ @uri = URI.parse("#{options[:ssl] ? 'https' : 'http' }://#{subdomain}.campfirenow.com")
+ if options[:proxy]
+ uri = URI.parse(options[:proxy])
+ @http = Net::HTTP::Proxy(uri.host, uri.port, uri.user, uri.password)
+ else
+ @http = Net::HTTP
+ end
+ @logged_in = false
+ end
+
+ # Log in to campfire using your +email+ and +password+
+ def login(email, password)
+ unless verify_response(post("login", :email_address => email, :password => password), :redirect_to => url_for(:only_path => false))
+ raise Error, "Campfire login failed"
+ end
+ # ensure that SSL is set if required on this account
+ raise SSLRequiredError, "Your account requires SSL" unless verify_response(get, :success)
+ @logged_in = true
+ end
+
+ # Returns true when successfully logged in
+ def logged_in?
+ @logged_in == true
+ end
+
+ def logout
+ returning verify_response(get("logout"), :redirect) do |result|
+ @logged_in = !result
+ end
+ end
+
+ # Get an array of all the available rooms
+ # TODO: detect rooms that are full (no link)
+ def rooms
+ Hpricot(get.body).search("//h2/a").collect do |a|
+ Room.new(self, room_id_from_url(a.attributes['href']), a.inner_html)
+ end
+ end
+
+ # Find a campfire room by name
+ def find_room_by_name(name)
+ rooms.detect {|room| room.name == name }
+ end
+
+ # Creates and returns a new Room with the given +name+ and optionally a +topic+
+ def create_room(name, topic = nil)
+ find_room_by_name(name) if verify_response(post("account/create/room?from=lobby", {:room => {:name => name, :topic => topic}}, :ajax => true), :success)
+ end
+
+ def find_or_create_room_by_name(name)
+ find_room_by_name(name) || create_room(name)
+ end
+
+ # List the users that are currently chatting in any room
+ def users(*room_names)
+ users = Hpricot(get.body).search("div.room").collect do |room|
+ if room_names.empty? || room_names.include?((room/"h2/a").inner_html)
+ room.search("//li.user").collect { |user| user.inner_html }
+ end
+ end
+ users.flatten.compact.uniq.sort
+ end
+
+ # Get the dates of the available transcripts by room
+ #
+ # campfire.available_transcripts
+ # #=> {"15840" => [#<Date: 4908311/2,0,2299161>, #<Date: 4908285/2,0,2299161>]}
+ #
+ def available_transcripts(room = nil)
+ url = "files%2Btranscripts"
+ url += "?room_id#{room}" if room
+ transcripts = (Hpricot(get(url).body) / ".transcript").inject({}) do |result,transcript|
+ link = (transcript / "a").first.attributes['href']
+ (result[room_id_from_url(link)] ||= []) << Date.parse(link.scan(/\/transcript\/(\d{4}\/\d{2}\/\d{2})/).to_s)
+ result
+ end
+ room ? transcripts[room.to_s] : transcripts
+ end
+
+ # Is the connection to campfire using ssl?
+ def ssl?
+ uri.scheme == 'https'
+ end
+
+ private
+
+ def room_id_from_url(url)
+ url.scan(/room\/(\d*)/).to_s
+ end
+
+ def url_for(*args)
+ options = {:only_path => true}.merge(args.last.is_a?(Hash) ? args.pop : {})
+ path = args.shift
+ "#{options[:only_path] ? '' : uri}/#{path}"
+ end
+
+ def post(path, data = {}, options = {})
+ perform_request(options) do
+ returning Net::HTTP::Post.new(url_for(path)) do |request|
+ request.add_field 'Content-Type', 'application/x-www-form-urlencoded'
+ request.set_form_data flatten(data)
+ end
+ end
+ end
+
+ def get(path = nil, options = {})
+ perform_request(options) { Net::HTTP::Get.new(url_for(path)) }
+ end
+
+ def prepare_request(request, options = {})
+ returning request do
+ request.add_field 'User-Agent', "Tinder/#{Tinder::VERSION::STRING} (http://tinder.rubyforge.org)"
+ request.add_field 'Cookie', @cookie if @cookie
+ if options[:ajax]
+ request.add_field 'X-Requested-With', 'XMLHttpRequest'
+ request.add_field 'X-Prototype-Version', '1.5.1.1'
+ end
+ end
+ end
+
+ def perform_request(options = {}, &block)
+ @request = prepare_request(yield, options)
+ http = @http.new(uri.host, uri.port)
+ http.use_ssl = ssl?
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE if ssl?
+ @response = returning http.request(@request) do |response|
+ @cookie = response['set-cookie'] if response['set-cookie']
+ end
+ end
+
+ # flatten a nested hash (:room => {:name => 'foobar'} to 'user[name]' => 'foobar')
+ def flatten(params)
+ params = params.dup
+ params.stringify_keys!.each do |k,v|
+ if v.is_a? Hash
+ params.delete(k)
+ v.each {|subk,v| params["#{k}[#{subk}]"] = v }
+ end
+ end
+ end
+
+ def verify_response(response, options = {})
+ if options.is_a?(Symbol)
+ codes = case options
+ when :success; [200]
+ when :redirect; 300..399
+ else raise(ArgumentError, "Unknown response #{options}")
+ end
+ codes.include?(response.code.to_i)
+ elsif options[:redirect_to]
+ verify_response(response, :redirect) && response['location'] == options[:redirect_to]
+ else
+ false
+ end
+ end
+
+ end
+end
202 lib/tinder/lib/tinder/room.rb
@@ -0,0 +1,202 @@
+module Tinder
+ # A campfire room
+ class Room
+ attr_reader :id, :name
+
+ def initialize(campfire, id, name = nil)
+ @campfire = campfire
+ @id = id
+ @name = name
+ end
+
+ # Join the room. Pass +true+ to join even if you've already joined.
+ def join(force = false)
+ @room = returning(get("room/#{id}")) do |room|
+ raise Error, "Could not join room" unless verify_response(room, :success)
+ @membership_key = room.body.scan(/\"membershipKey\": \"([a-z0-9]+)\"/).to_s
+ @user_id = room.body.scan(/\"userID\": (\d+)/).to_s
+ @last_cache_id = room.body.scan(/\"lastCacheID\": (\d+)/).to_s
+ @timestamp = room.body.scan(/\"timestamp\": (\d+)/).to_s
+ @idle_since = Time.now
+ end if @room.nil? || force
+ ping
+ true
+ end
+
+ # Leave a room
+ def leave
+ returning verify_response(post("room/#{id}/leave"), :redirect) do
+ @room, @membership_key, @user_id, @last_cache_id, @timestamp, @idle_since = nil
+ end
+ end
+
+ # Toggle guest access on or off
+ def toggle_guest_access
+ # re-join the room to get the guest url
+ verify_response(post("room/#{id}/toggle_guest_access"), :success) && join(true)
+ end
+
+ # Get the url for guest access
+ def guest_url
+ join
+ link = (Hpricot(@room.body)/"#guest_access h4").first
+ link.inner_html if link
+ end
+
+ def guest_access_enabled?
+ !guest_url.nil?
+ end
+
+ # The invite code use for guest
+ def guest_invite_code
+ guest_url.scan(/\/(\w*)$/).to_s
+ end
+
+ # Change the name of the room
+ def name=(name)
+ @name = name if verify_response(post("account/edit/room/#{id}", { :room => { :name => name }}, :ajax => true), :success)
+ end
+ alias_method :rename, :name=
+
+ # Change the topic
+ def topic=(topic)
+ topic if verify_response(post("room/#{id}/change_topic", { 'room' => { 'topic' => topic }}, :ajax => true), :success)
+ end
+
+ # Get the current topic
+ def topic
+ join
+ h = (Hpricot(@room.body)/"#topic")
+ if h
+ (h/:span).remove
+ h.inner_text.strip
+ end
+ end
+
+ # Lock the room to prevent new users from entering and to disable logging
+ def lock
+ verify_response(post("room/#{id}/lock", {}, :ajax => true), :success)
+ end
+
+ # Unlock the room
+ def unlock
+ verify_response(post("room/#{id}/unlock", {}, :ajax => true), :success)
+ end
+
+ def ping(force = false)
+ returning verify_response(post("room/#{id}/tabs", { }, :ajax => true), :success) do
+ @idle_since = Time.now
+ end if @idle_since < 1.minute.ago || force
+ end
+
+ def destroy
+ verify_response(post("account/delete/room/#{id}"), :success)
+ end
+
+ # Post a new message to the chat room
+ def speak(message, options = {})
+ message if verify_response(post("room/#{id}/speak", {:message => message,
+ :t => Time.now.to_i}.merge(options), :ajax => true), :success)
+ end
+
+ def paste(message)
+ speak message, :paste => true
+ end
+
+ # Get the list of users currently chatting for this room
+ def users
+ @campfire.users name
+ end
+
+ # Get and array of the messages that have been posted to the room. Each
+ # messages is a hash with:
+ # * +:person+: the display name of the person that posted the message
+ # * +:message+: the body of the message
+ # * +:user_id+: Campfire user id
+ # * +:id+: Campfire message id
+ #
+ # room.listen
+ # #=> [{:person=>"Brandon", :message=>"I'm getting very sleepy", :user_id=>"148583", :id=>"16434003"}]
+ #
+ # Called without a block, listen will return an array of messages that have been
+ # posted since you joined. listen also takes an optional block, which then polls
+ # for new messages every 5 seconds and calls the block for each message.
+ #
+ # room.listen do |m|
+ # room.speak "#{m[:person]}, Go away!" if m[:message] =~ /Java/i
+ # end
+ #
+ def listen(interval = 5)
+ join
+ if block_given?
+ catch(:stop_listening) do
+ trap('INT') { throw :stop_listening }
+ loop do
+ ping
+ self.messages.each {|msg| yield msg }
+ sleep interval
+ end
+ end
+ else
+ self.messages
+ end
+ end
+
+ # Get the dates for the available transcripts for this room
+ def available_transcripts
+ @campfire.available_transcripts(id)
+ end
+
+ # Get the transcript for the given date (Returns a hash in the same format as #listen)
+ #
+ # room.transcript(room.available_transcripts.first)
+ # #=> [{:message=>"foobar!", :user_id=>"99999", :person=>"Brandon", :id=>"18659245"}]
+ #
+ def transcript(date)
+ (Hpricot(get("room/#{id}/transcript/#{date.to_date.strftime('%Y/%m/%d')}").body) / ".message").collect do |message|
+ person = (message / '.person span').first
+ body = (message / '.body div').first
+ {:id => message.attributes['id'].scan(/message_(\d+)/).to_s,
+ :person => person ? person.inner_html : nil,
+ :user_id => message.attributes['class'].scan(/user_(\d+)/).to_s,
+ :message => body ? body.inner_html : nil
+ }
+ end
+ end
+
+ protected
+
+ def messages
+ returning [] do |messages|
+ response = post("poll.fcgi", {:l => @last_cache_id, :m => @membership_key,
+ :s => @timestamp, :t => "#{Time.now.to_i}000"}, :ajax => true)
+ if response.body.length > 1
+ lines = response.body.split("\r\n")
+
+ if lines.length > 0
+ @last_cache_id = lines.pop.scan(/chat.poller.lastCacheID = (\d+)/).to_s
+ lines.each do |msg|
+ unless msg.match(/timestamp_message/)
+ if msg.length > 0
+ messages << {
+ :id => msg.scan(/message_(\d+)/).to_s,
+ :user_id => msg.scan(/user_(\d+)/).to_s,
+ :person => msg.scan(/\\u003Ctd class=\\"person\\"\\u003E(?:\\u003Cspan\\u003E)?(.+?)(?:\\u003C\/span\\u003E)?\\u003C\/td\\u003E/).to_s,
+ :message => msg.scan(/\\u003Ctd class=\\"body\\"\\u003E\\u003Cdiv\\u003E(.+?)\\u003C\/div\\u003E\\u003C\/td\\u003E/).to_s
+ }
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+
+ [:post, :get, :verify_response].each do |method|
+ define_method method do |*args|
+ @campfire.send method, *args
+ end
+ end
+
+ end
+end
9 lib/tinder/lib/tinder/version.rb
@@ -0,0 +1,9 @@
+module Tinder #:nodoc:
+ module VERSION #:nodoc:
+ MAJOR = 0
+ MINOR = 1
+ TINY = 6
+
+ STRING = [MAJOR, MINOR, TINY].join('.')
+ end
+end
101 lib/tinder/site/index.html
@@ -0,0 +1,101 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
+ <title>Tinder</title>
+ <link rel="stylesheet" type="text/css" href="http://opensoul.org/stylesheets/code.css" />
+ <link rel="stylesheet" type="text/css" href="stylesheets/style.css" />
+ <link href="http://opensoul.org/stylesheets/ci.css" rel="stylesheet" type="text/css" />
+ <script src="http://opensoul.org/javascripts/code_highlighter.js" type="text/javascript"></script>
+ <script src="http://opensoul.org/javascripts/ruby.js" type="text/javascript"></script>
+</head>
+
+<body>
+<div id="collectiveidea">
+ <a href="http://collectiveidea.com"><img src="http://opensoul.org/images/header_logo.gif" alt="Collective Idea" class="logo" width="17" height="22" /></a>
+ <ul class="links">
+ <li><a href="http://daniel.collectiveidea.com/blog">Daniel</a></li>
+ <li><a href="http://opensoul.org">Brandon</a></li>
+ <li class="name"><a href="http://collectiveidea.com"><img src="http://opensoul.org/images/header_collectiveidea.gif" alt="Collective Idea" width="123" height="21" /></a></li>
+ </ul>
+</div>
+<div id="main">
+ <div id="header">
+ <h1><a href="/">Tinder</a></h1>
+ <p>Getting the campfire started</p>
+ <ul id="nav">
+ <li><a href="tinder">API Docs</a></li>
+ <li><a href="http://rubyforge.org/projects/tinder">RubyForge</a></li>
+ <li><a href="http://opensoul.org/tags/tinder">Blog</a></li>
+ </ul>
+ </div>
+ <div id="content">
+ <p>Tinder is an API for interfacing with <a href="http://campfirenow.com">Campfire</a>, the 37Signals chat application.</p>
+ <h2>Example</h2>
+
+ <pre><code class="ruby">campfire = Tinder::Campfire.new 'mysubdomain'
+campfire.login 'myemail@example.com', 'mypassword'</code></pre>
+
+<h3>Create, find and destroy rooms</h3>
+<pre><code class="ruby">room = campfire.create_room 'New Room', 'My new campfire room to test tinder'
+room = campfire.find_room_by_name 'Other Room'
+room.destroy</code></pre>
+
+<h3>Speak and Paste</h3>
+<pre><code class="ruby">room.speak 'Hello world!'
+room.paste File.read(&quot;path/to/your/file.txt&quot;)</code></pre>
+
+<h3>Listening</h3>
+<pre><code class="ruby">room.listen
+#=&gt; [{:person=&gt;&quot;Brandon&quot;, :message=&gt;&quot;I'm getting very sleepy&quot;, :user_id=&gt;&quot;148583&quot;, :id=&gt;&quot;16434003&quot;}]
+
+# or in block form
+room.listen do |m|
+ room.speak 'Welcome!' if m[:message] == /hello/
+end</code></pre>
+
+<h3>Guest Access</h3>
+<pre><code class="ruby">room.toggle_guest_access
+room.guest_url #=> http://mysubdomain.campfirenow.com/11111
+room.guest_invite_code #=> 11111</code></pre>
+
+<h3>Change the name and topic</h3>
+<pre><code class="ruby">room.name = 'Tinder Demo'
+room.topic = 'Showing how to change the room name and topic with tinder…'</code></pre>
+
+<h3>Users</h3>
+<pre><code class="ruby">room.users
+campfire.users # users in all rooms</code></pre>
+
+<h3>Transcripts</h3>
+<pre><code class="ruby">transcript = room.transcript(room.available_transcripts.first)
+#=&gt; [{:message=&gt;&quot;foobar!&quot;, :user_id=&gt;&quot;99999&quot;, :person=&gt;&quot;Brandon&quot;, :id=&gt;&quot;18659245&quot;}]
+</code></pre>
+
+ <p>See the <a href="tinder">API documentation</a> for more details.</p>
+
+ <h2>Installation</h2>
+
+ <p>Tinder can be installed as a gem or a Rails plugin. Install the gem by executing:</p>
+
+ <pre>gem install tinder</pre>
+
+ <p>Or, download it from <a href="http://rubyforge.org/frs/?group_id=2922">RubyForge</a>.</p>
+
+ <h2>Source</h2>
+
+ <p>Contributions are welcome and appreciated! The source is available from:</p>
+
+ <pre>http://github.com/collectiveidea/tinder</pre>
+ </div>
+</div>
+<script src="http://www.google-analytics.com/urchin.js" type="text/javascript">
+</script>
+<script type="text/javascript">
+_uacct = "UA-194397-8";
+urchinTracker();
+</script>
+</body>
+</html>
81 lib/tinder/site/stylesheets/style.css
@@ -0,0 +1,81 @@
+body {
+ font-family: "Lucida Grande", Helvetica, Arial, sans-serif;
+ font-size: 76%;
+ background: #2A2A2A;
+ margin: 0;
+ padding: 0;
+}
+
+#collectiveidea {
+ border-bottom: 1px solid #444;
+}
+
+a {
+ color: #2D5385;
+}
+
+#main {
+ background-color: #FFF;
+ width: 700px;
+ margin: 0 auto;
+ border: 5px #CCC;
+ border-left-style: solid;
+ border-right-style: solid;
+ padding: 0 1em;
+}
+
+#header {
+ position: relative;
+ border-bottom: 1px solid #999;
+ padding: 1em;
+}
+
+#header h1 {
+ margin: 0;
+ padding: 0;
+ color: #2D5385;
+}
+
+#header h1 a {
+ text-decoration: none;
+}
+
+#header p {
+ margin: 0;
+ padding: 0;
+ font-size: 0.8em;
+ color: #999;
+}
+
+#nav {
+ list-style: none;
+ position: absolute;
+ right: 0;
+ top: 0.6em;
+}
+#nav li {
+ display: inline;
+ padding: 0 0.5em;
+}
+
+#content {
+ padding: 1em 0;
+}
+
+pre {
+ background-color: #2A2A2A;
+}
+
+dl {
+ background-color: #DDD;
+ padding: 1em;
+ border: 1px solid #CCC;
+}
+dl .pronunciation {
+ color: #C00;
+}
+dl .description {
+ text-transform: uppercase;
+ font-size: 0.8em;
+ font-family: fixed;
+}
113 lib/tinder/spec/campfire_spec.rb
@@ -0,0 +1,113 @@
+require File.dirname(__FILE__) + '/spec_helper.rb'
+
+context "Preparing a campfire request" do
+ setup do
+ @campfire = Tinder::Campfire.new("foobar")
+ @request = Net::HTTP::Get.new("does_not_matter")
+ end
+
+ def prepare_request
+ @campfire.send(:prepare_request, @request)
+ end
+
+ specify "should return the request" do
+ prepare_request.should equal(@request)
+ end
+
+ specify "should set the cookie" do
+ @campfire.instance_variable_set("@cookie", "foobar")
+ prepare_request['Cookie'].should == 'foobar'
+ end
+
+ specify "should set the user agent" do
+ prepare_request['User-Agent'].should =~ /^Tinder/
+ end
+end
+
+# context "Performing a campfire request" do
+#
+# setup do
+# @response = mock("response")
+# Net::HTTP.any_instance.stubs(:request).returns(response)
+# request = Net::HTTP::Get.new("does_not_matter")
+# response.expects(:[]).with('set-cookie').and_return('foobar')
+# @campfire.send(:perform_request) { request }
+# end
+#
+# specify "should set cookie" do
+# @campfire.instance_variable_get("@cookie").should == 'foobar'
+# end
+#
+# end
+
+context "Verifying a 200 response" do
+
+ setup do
+ @campfire = Tinder::Campfire.new("foobar")
+ @response.should_receive(:code).and_return(200)
+ end
+
+ specify "should return true when expecting success" do
+ @campfire.send(:verify_response, @response, :success).should equal(true)
+ end
+
+ specify "should return false when expecting a redirect" do
+ @campfire.send(:verify_response, @response, :redirect).should equal(false)
+ end
+
+ specify "should return false when expecting a redirect to a specific path" do
+ @campfire.send(:verify_response, @response, :redirect_to => '/foobar').should equal(false)
+ end
+
+end
+
+context "Verifying a 302 response" do
+
+ setup do
+ @campfire = Tinder::Campfire.new("foobar")
+ @response.should_receive(:code).and_return(302)
+ end
+
+ specify "should return true when expecting redirect" do
+ @campfire.send(:verify_response, @response, :redirect).should equal(true)
+ end
+
+ specify "should return false when expecting success" do
+ @campfire.send(:verify_response, @response, :success).should equal(false)
+ end
+
+ specify "should return true when expecting a redirect to a specific path" do
+ @response.should_receive(:[]).with('location').and_return("/foobar")
+ @campfire.send(:verify_response, @response, :redirect_to => '/foobar').should equal(true)
+ end
+
+ specify "should return false when redirecting to a different path than expected" do
+ @response.should_receive(:[]).with('location').and_return("/baz")
+ @campfire.send(:verify_response, @response, :redirect_to => '/foobar').should equal(false)
+ end
+
+end
+
+context "A failed login" do
+
+ setup do
+ @campfire = Tinder::Campfire.new 'foobar'
+ @response = mock("response")
+ @campfire.should_receive(:post).and_return(@response)
+ @response.should_receive(:code).and_return("302")
+ @response.should_receive(:[]).with("location").and_return("/login")
+ end
+
+ specify "should raise an error" do
+ lambda do
+ @campfire.login "doesn't", "matter"
+ end.should raise_error(Tinder::Error)
+ end
+
+ specify "should not set logged in status" do
+ @campfire.login 'foo', 'bar' rescue
+ @campfire.logged_in?.should equal(false)
+ end
+
+
+end
3 lib/tinder/spec/spec_helper.rb
@@ -0,0 +1,3 @@
+require 'rubygems'
+require 'spec'
+require File.dirname(__FILE__) + '/../lib/tinder.rb'
58 lib/tinder/test/remote/remote_campfire_test.rb
@@ -0,0 +1,58 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class RemoteCampfireTest < Test::Unit::TestCase
+
+ def setup
+ # @subdomain = 'domain'
+ # @user, @pass = 'email@example.com', 'password'
+ @ssl = false
+ raise "Set your campfire credentials before running the remote tests" unless @user && @pass && @subdomain
+ @campfire = Tinder::Campfire.new @subdomain, :ssl => @ssl
+ end
+
+ def test_ssl_required
+ if @ssl
+ campfire = Tinder::Campfire.new @subdomain
+ assert_raises(Tinder::SSLRequiredError) do
+ campfire.login(@user, @pass)
+ end
+ end
+ end
+
+ def test_create_and_delete_room
+ assert login
+ assert @campfire.logged_in?
+
+ room = @campfire.create_room("Testing#{Time.now.to_i}")
+
+ assert_instance_of Tinder::Room, room
+ assert_not_nil room.id
+
+ room.name = "new name"
+ assert_equal "new name", room.name
+
+ room.destroy
+ assert_nil @campfire.find_room_by_name(room.name)
+
+ assert @campfire.logout
+ ensure
+ room.destroy rescue nil
+ end
+
+ def test_failed_login
+ assert_raises(Tinder::Error) { @campfire.login(@user, 'notmypassword') }
+ assert !@campfire.logged_in?
+ end
+
+ def test_find_nonexistent_room
+ login
+ assert_nil @campfire.find_room_by_name('No Room Should Have This Name')
+ end
+
+private
+
+ def login(user = @user, pass = @pass)
+ @campfire.login(user, pass)
+ end
+
+end
7 lib/tinder/test/test_helper.rb
@@ -0,0 +1,7 @@
+$:.unshift(File.dirname(__FILE__) + '/mocks')
+require 'test/unit'
+require 'rubygems'
+require 'active_support'
+require File.dirname(__FILE__) + '/../lib/tinder.rb'
+require 'mocha'
+require 'stubba'

0 comments on commit b1e750b

Please sign in to comment.
Something went wrong with that request. Please try again.