collectiveidea-tinder 1.2.2

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ pkg
2
+ rdoc
3
+ test/remote/credentials.rb
data/CHANGELOG.txt ADDED
@@ -0,0 +1,39 @@
1
+ 1.2.1 - 2009-08-27
2
+ * Fixes for listening after campfire updates [Jordan Byron]
3
+
4
+ 1.2.0 - 2009-01-28
5
+ * Get the list of available files [Christopher MacGown]
6
+ * Upload files [Joshua Wand]
7
+ * Find rooms even when full [Josh Owens]
8
+ * Join rooms as a guest [Ian Lesperance]
9
+
10
+ 1.1.7 - 2008-07-24
11
+ * Don't join the room when only speaking [Brian Donovan]
12
+ * Added support for HTTP proxies
13
+ * Fix listening for messages that contain URLs [Jared Kuolt]
14
+
15
+ 0.1.6 - 2008-03-07
16
+ * Added Room#topic for getting the current topic [Even Weaver]
17
+ * Trap INT in #listen(&block) [borrowed from Chris Shea's Pyre]
18
+
19
+ 0.1.5 - 2008-01-25
20
+ * Fixed Room#listen, which was broken by latest Campfire deploy
21
+ * Fixed timeout when listening but not speaking that will eventually log you out [Clinton R. Nixon]
22
+
23
+ 0.1.4 - 2007-07-23
24
+ * Support for transcripts
25
+ * Fixed Room#leave, which was broken by a Campfire deployment [Andy Smith]
26
+
27
+ 0.1.3 - 2007-02-12
28
+ * added ssl support [Tero Parviainen]
29
+
30
+ 0.1.2 - 2007-01-27
31
+ * fixed bug preventing #listen from working without a block
32
+
33
+ 0.1.1 - 2007-01-27
34
+ * fix bug preventing speak from working
35
+ * incorporated "watching" from http://soylentfoo.jnewland.com/articles/2006/12/07/updates-to-marshmallow-the-campfire-bot
36
+
37
+ 0.1.0 - 2007-01-23
38
+ * Initial release as gem
39
+ * Get the users in a room [Tero Parviainen]
data/Manifest.txt ADDED
@@ -0,0 +1,10 @@
1
+ CHANGELOG.txt
2
+ Manifest.txt
3
+ README.txt
4
+ Rakefile
5
+ init.rb
6
+ lib/tinder.rb
7
+ lib/tinder/campfire.rb
8
+ lib/tinder/multipart.rb
9
+ lib/tinder/room.rb
10
+ lib/tinder/version.rb
data/README.txt ADDED
@@ -0,0 +1,48 @@
1
+ = Tinder - get the Campfire started
2
+
3
+ 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.
4
+
5
+ == Usage
6
+
7
+ campfire = Campfire.new 'mysubdomain'
8
+ campfire.login 'myemail@example.com', 'mypassword'
9
+
10
+ room = campfire.create_room 'New Room', 'My new campfire room to test tinder'
11
+ room.rename 'New Room Name'
12
+ room.speak 'Hello world!'
13
+ room.paste "my pasted\ncode"
14
+ room.destroy
15
+
16
+ room = campfire.find_room_by_guest_hash 'abc123', 'John Doe'
17
+ room.speak 'Hello world!'
18
+
19
+ See the RDoc for more details.
20
+
21
+ == Installation
22
+
23
+ Tinder can be installed as a gem or a Rails plugin:
24
+
25
+ gem install tinder
26
+
27
+ script/plugin install git://github.com/collectiveidea/tinder.git
28
+
29
+ == How to contribute
30
+
31
+ If you find what looks like a bug:
32
+
33
+ 1. Check the GitHub issue tracker to see if anyone else has had the same issue.
34
+ http://github.com/collectiveidea/tinder/issues/
35
+ 2. If you don't see anything, create an issue with information on how to reproduce it.
36
+
37
+ If you want to contribute an enhancement or a fix:
38
+
39
+ 1. Fork the project on github.
40
+ http://github.com/collectiveidea/tinder
41
+ 2. Make your changes with tests.
42
+ 3. Commit the changes without making changes to the Rakefile, VERSION, or any other files that aren't related to your enhancement or fix
43
+ 4. Send a pull request.
44
+
45
+ == ToDo
46
+
47
+ * Tests! (unit and remote)
48
+ * Marshmallow-style integration scripts for exception notification and continuous integration
data/Rakefile ADDED
@@ -0,0 +1,68 @@
1
+ begin
2
+ require 'jeweler'
3
+ Jeweler::Tasks.new do |gem|
4
+ gem.name = "tinder"
5
+ gem.summary = "An (unofficial) Campfire API"
6
+ gem.description = "An API for interfacing with Campfire, the 37Signals chat application."
7
+ gem.authors = ['Brandon Keepers']
8
+ gem.email = 'brandon@opensoul.org'
9
+ gem.homepage = 'http://github.com/collectiveidea/tinder'
10
+ gem.rubyforge_project = "tinder"
11
+ gem.add_dependency "activesupport"
12
+ gem.add_dependency "hpricot"
13
+ gem.add_dependency "mime-types"
14
+ gem.add_development_dependency "rspec"
15
+ end
16
+ Jeweler::GemcutterTasks.new
17
+ Jeweler::RubyforgeTasks.new do |rubyforge|
18
+ rubyforge.doc_task = "rdoc"
19
+ end
20
+ rescue LoadError
21
+ puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
22
+ end
23
+
24
+ require 'rake/testtask'
25
+ Rake::TestTask.new(:test) do |test|
26
+ test.libs << 'lib' << 'test'
27
+ test.pattern = 'test/**/*_test.rb'
28
+ test.verbose = true
29
+ end
30
+
31
+ begin
32
+ require 'rcov/rcovtask'
33
+ Rcov::RcovTask.new do |test|
34
+ test.libs << 'test'
35
+ test.pattern = 'test/**/*_test.rb'
36
+ test.verbose = true
37
+ end
38
+ rescue LoadError
39
+ task :rcov do
40
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
41
+ end
42
+ end
43
+
44
+ task :test => :check_dependencies
45
+
46
+ require 'rake/rdoctask'
47
+ Rake::RDocTask.new do |rdoc|
48
+ if File.exist?('VERSION')
49
+ version = File.read('VERSION')
50
+ else
51
+ version = ""
52
+ end
53
+
54
+ rdoc.rdoc_dir = 'rdoc'
55
+ rdoc.title = "tinder #{version}"
56
+ rdoc.rdoc_files.include('README*')
57
+ rdoc.rdoc_files.include('lib/**/*.rb')
58
+ end
59
+
60
+ require 'spec/rake/spectask'
61
+ desc "Run the specs under spec"
62
+ Spec::Rake::SpecTask.new do |t|
63
+ t.spec_opts = ['--options', "spec/spec.opts"]
64
+ t.spec_files = FileList['spec/**/*_spec.rb']
65
+ end
66
+
67
+ desc "Run tests"
68
+ task :default => [:spec, :test]
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.2.2
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'tinder'
data/lib/tinder.rb ADDED
@@ -0,0 +1,16 @@
1
+ require 'rubygems'
2
+ require 'active_support'
3
+ require 'uri'
4
+ require 'net/http'
5
+ require 'net/https'
6
+ require 'open-uri'
7
+ require 'hpricot'
8
+
9
+ require 'tinder/multipart'
10
+ require 'tinder/campfire'
11
+ require 'tinder/room'
12
+
13
+ module Tinder
14
+ class Error < StandardError; end
15
+ class SSLRequiredError < Error; end
16
+ end
@@ -0,0 +1,207 @@
1
+ module Tinder
2
+
3
+ # == Usage
4
+ #
5
+ # campfire = Tinder::Campfire.new 'mysubdomain'
6
+ # campfire.login 'myemail@example.com', 'mypassword'
7
+ #
8
+ # room = campfire.create_room 'New Room', 'My new campfire room to test tinder'
9
+ # room.speak 'Hello world!'
10
+ # room.destroy
11
+ #
12
+ # room = campfire.find_room_by_guest_hash 'abc123', 'John Doe'
13
+ # room.speak 'Hello world!'
14
+ class Campfire
15
+ attr_reader :subdomain, :uri
16
+
17
+ # Create a new connection to the campfire account with the given +subdomain+.
18
+ #
19
+ # == Options:
20
+ # * +:ssl+: use SSL for the connection, which is required if you have a Campfire SSL account.
21
+ # Defaults to false
22
+ # * +:proxy+: a proxy URI. (e.g. :proxy => 'http://user:pass@example.com:8000')
23
+ #
24
+ # c = Tinder::Campfire.new("mysubdomain", :ssl => true)
25
+ def initialize(subdomain, options = {})
26
+ options = { :ssl => false }.merge(options)
27
+ @cookie = nil
28
+ @subdomain = subdomain
29
+ @uri = URI.parse("#{options[:ssl] ? 'https' : 'http' }://#{subdomain}.campfirenow.com")
30
+ if options[:proxy]
31
+ uri = URI.parse(options[:proxy])
32
+ @http = Net::HTTP::Proxy(uri.host, uri.port, uri.user, uri.password)
33
+ else
34
+ @http = Net::HTTP
35
+ end
36
+ @logged_in = false
37
+ end
38
+
39
+ # Log in to campfire using your +email+ and +password+
40
+ def login(email, password)
41
+ unless verify_response(post("login", :email_address => email, :password => password), :redirect_to => url_for(:only_path => false))
42
+ raise Error, "Campfire login failed"
43
+ end
44
+ # ensure that SSL is set if required on this account
45
+ raise SSLRequiredError, "Your account requires SSL" unless verify_response(get, :success)
46
+ @logged_in = true
47
+ end
48
+
49
+ # Returns true when successfully logged in
50
+ def logged_in?
51
+ @logged_in == true
52
+ end
53
+
54
+ def logout
55
+ returning verify_response(get("logout"), :redirect) do |result|
56
+ @logged_in = !result
57
+ end
58
+ end
59
+
60
+ # Get an array of all the available rooms
61
+ # TODO: detect rooms that are full (no link)
62
+ def rooms
63
+ Hpricot(get.body).search("//div.room").collect do |a|
64
+ name = a.search("//h2/a").inner_html.strip
65
+ name = a.search("//h2").inner_html.strip if name.empty?
66
+ Room.new(self, room_id_from_element(a.attributes['id']), name)
67
+ end
68
+ end
69
+
70
+ # Find a campfire room by name
71
+ def find_room_by_name(name)
72
+ rooms.detect {|room| room.name == name }
73
+ end
74
+
75
+ # Find a campfire room by its guest hash
76
+ def find_room_by_guest_hash(hash, name)
77
+ res = post(hash, :name => name)
78
+
79
+ Room.new(self, room_id_from_url(res['location'])) if verify_response(res, :redirect)
80
+ end
81
+
82
+ # Creates and returns a new Room with the given +name+ and optionally a +topic+
83
+ def create_room(name, topic = nil)
84
+ find_room_by_name(name) if verify_response(post("account/create/room?from=lobby", {:room => {:name => name, :topic => topic}}, :ajax => true), :success)
85
+ end
86
+
87
+ def find_or_create_room_by_name(name)
88
+ find_room_by_name(name) || create_room(name)
89
+ end
90
+
91
+ # List the users that are currently chatting in any room
92
+ def users(*room_names)
93
+ users = Hpricot(get.body).search("div.room").collect do |room|
94
+ if room_names.empty? || room_names.include?((room/"h2/a").inner_html)
95
+ room.search("//li.user").collect { |user| user.inner_html }
96
+ end
97
+ end
98
+ users.flatten.compact.uniq.sort
99
+ end
100
+
101
+ # Get the dates of the available transcripts by room
102
+ #
103
+ # campfire.available_transcripts
104
+ # #=> {"15840" => [#<Date: 4908311/2,0,2299161>, #<Date: 4908285/2,0,2299161>]}
105
+ #
106
+ def available_transcripts(room = nil)
107
+ url = "files%2Btranscripts"
108
+ url += "?room_id#{room}" if room
109
+ transcripts = (Hpricot(get(url).body) / ".transcript").inject({}) do |result,transcript|
110
+ link = (transcript / "a").first.attributes['href']
111
+ (result[room_id_from_url(link)] ||= []) << Date.parse(link.scan(/\/transcript\/(\d{4}\/\d{2}\/\d{2})/).to_s)
112
+ result
113
+ end
114
+ room ? transcripts[room.to_s] : transcripts
115
+ end
116
+
117
+ # Is the connection to campfire using ssl?
118
+ def ssl?
119
+ uri.scheme == 'https'
120
+ end
121
+
122
+ private
123
+
124
+ def room_id_from_url(url)
125
+ url.scan(/room\/(\d*)/).to_s
126
+ end
127
+
128
+ def room_id_from_element(element)
129
+ element.split("_").last
130
+ end
131
+
132
+ def url_for(*args)
133
+ options = {:only_path => true}.merge(args.last.is_a?(Hash) ? args.pop : {})
134
+ path = args.shift
135
+ "#{options[:only_path] ? '' : uri}/#{path}"
136
+ end
137
+
138
+ def post(path, data = {}, options = {})
139
+ perform_request(options) do
140
+ returning Net::HTTP::Post.new(url_for(path)) do |request|
141
+ if options[:multipart]
142
+ request.body = data
143
+ else
144
+ request.set_form_data(flatten(data))
145
+ end
146
+ end
147
+ end
148
+ end
149
+
150
+ def get(path = nil, options = {})
151
+ perform_request(options) { Net::HTTP::Get.new(url_for(path)) }
152
+ end
153
+
154
+ def prepare_request(request, options = {})
155
+ returning request do
156
+ request.add_field 'User-Agent', "Tinder (http://tinder.rubyforge.org)"
157
+ request.add_field 'Cookie', @cookie if @cookie
158
+ request.add_field 'X-Requested-With', 'XMLHttpRequest'
159
+ if options[:ajax]
160
+ request.add_field 'X-Prototype-Version', '1.5.1.1'
161
+ end
162
+ if options[:multipart]
163
+ request.add_field 'Content-Type', 'multipart/form-data, boundary=' + Multipart::MultipartPost::BOUNDARY + " "
164
+ else
165
+ request.add_field 'Content-Type', 'application/x-www-form-urlencoded'
166
+ end
167
+ end
168
+ end
169
+
170
+ def perform_request(options = {}, &block)
171
+ @request = prepare_request(yield, options)
172
+ http = @http.new(uri.host, uri.port)
173
+ http.use_ssl = ssl?
174
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE if ssl?
175
+ @response = returning http.request(@request) do |response|
176
+ @cookie = response['set-cookie'] if response['set-cookie']
177
+ end
178
+ end
179
+
180
+ # flatten a nested hash (:room => {:name => 'foobar'} to 'user[name]' => 'foobar')
181
+ def flatten(params)
182
+ params = params.dup
183
+ params.stringify_keys!.each do |k,v|
184
+ if v.is_a? Hash
185
+ params.delete(k)
186
+ v.each {|subk,v| params["#{k}[#{subk}]"] = v }
187
+ end
188
+ end
189
+ end
190
+
191
+ def verify_response(response, options = {})
192
+ if options.is_a?(Symbol)
193
+ codes = case options
194
+ when :success; [200]
195
+ when :redirect; 300..399
196
+ else raise(ArgumentError, "Unknown response #{options}")
197
+ end
198
+ codes.include?(response.code.to_i)
199
+ elsif options[:redirect_to]
200
+ verify_response(response, :redirect) && response['location'] == options[:redirect_to]
201
+ else
202
+ false
203
+ end
204
+ end
205
+
206
+ end
207
+ end
@@ -0,0 +1,64 @@
1
+ require 'rubygems'
2
+ require 'mime/types'
3
+ require 'net/http'
4
+ require 'cgi'
5
+
6
+ module Multipart #:nodoc:
7
+ # From: http://deftcode.com/code/flickr_upload/multipartpost.rb
8
+ ## Helper class to prepare an HTTP POST request with a file upload
9
+ ## Mostly taken from
10
+ #http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/113774
11
+ ### WAS:
12
+ ## Anything that's broken and wrong probably the fault of Bill Stilwell
13
+ ##(bill@marginalia.org)
14
+ ### NOW:
15
+ ## Everything wrong is due to keith@oreilly.com
16
+
17
+ class Param #:nodoc:
18
+ attr_accessor :k, :v
19
+ def initialize(k, v)
20
+ @k = k
21
+ @v = v
22
+ end
23
+
24
+ def to_multipart
25
+ "Content-Disposition: form-data; name=\"#{k}\"\r\n\r\n#{v}\r\n"
26
+ end
27
+ end
28
+
29
+ class FileParam #:nodoc:
30
+ attr_accessor :k, :filename, :content
31
+ def initialize(k, filename, content)
32
+ @k = k
33
+ @filename = filename
34
+ @content = content
35
+ end
36
+
37
+ def to_multipart
38
+ "Content-Disposition: form-data; name=\"#{k}\"; filename=\"#{filename}\"\r\n" +
39
+ "Content-Transfer-Encoding: binary\r\n" +
40
+ "Content-Type: #{MIME::Types.type_for(@filename)}\r\n\r\n" +
41
+ @content + "\r\n"
42
+ end
43
+ end
44
+
45
+ class MultipartPost #:nodoc:
46
+ BOUNDARY = 'campfire-is-awesome'
47
+ HEADER = {"Content-type" => "multipart/form-data, boundary=" + BOUNDARY + " "}
48
+ TIMEOUT_SECONDS = 30
49
+
50
+ attr_accessor :params, :query, :headers
51
+ def initialize(params)
52
+ @params = params
53
+ @query = {}
54
+ self.prepare_query
55
+ end
56
+
57
+ def prepare_query()
58
+ @query = @params.map do |k,v|
59
+ param = v.respond_to?(:read) ? FileParam.new(k, v.path, v.read) : Param.new(k, v)
60
+ "--#{BOUNDARY}\r\n#{param.to_multipart}"
61
+ end.join("") + "--#{BOUNDARY}--"
62
+ end
63
+ end
64
+ end