collectiveidea-tinder 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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