sj-tinder 1.9.3
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.
- checksums.yaml +15 -0
- data/.gemtest +0 -0
- data/.gitignore +5 -0
- data/.rspec +3 -0
- data/.travis.yml +13 -0
- data/CHANGELOG.txt +79 -0
- data/Gemfile +7 -0
- data/MIT-LICENSE +20 -0
- data/README.markdown +45 -0
- data/Rakefile +9 -0
- data/init.rb +1 -0
- data/lib/faraday/response/raise_on_authentication_failure.rb +10 -0
- data/lib/faraday/response/remove_whitespace.rb +10 -0
- data/lib/tinder.rb +20 -0
- data/lib/tinder/campfire.rb +76 -0
- data/lib/tinder/connection.rb +115 -0
- data/lib/tinder/room.rb +307 -0
- data/lib/tinder/version.rb +4 -0
- data/site/index.html +101 -0
- data/site/stylesheets/style.css +77 -0
- data/spec/fixtures/rooms.json +18 -0
- data/spec/fixtures/rooms/recent.json +19 -0
- data/spec/fixtures/rooms/room80749.json +21 -0
- data/spec/fixtures/rooms/room80751.json +21 -0
- data/spec/fixtures/rooms/show.json +21 -0
- data/spec/fixtures/users/me.json +11 -0
- data/spec/spec_helper.rb +27 -0
- data/spec/tinder/campfire_spec.rb +93 -0
- data/spec/tinder/connection_spec.rb +104 -0
- data/spec/tinder/room_spec.rb +330 -0
- data/tinder.gemspec +31 -0
- metadata +246 -0
data/lib/tinder/room.rb
ADDED
@@ -0,0 +1,307 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
require 'time'
|
3
|
+
|
4
|
+
module Tinder
|
5
|
+
# A campfire room
|
6
|
+
class Room
|
7
|
+
attr_reader :id, :name
|
8
|
+
|
9
|
+
def initialize(connection, attributes = {})
|
10
|
+
@connection = connection
|
11
|
+
@id = attributes['id']
|
12
|
+
@name = attributes['name']
|
13
|
+
@loaded = false
|
14
|
+
end
|
15
|
+
|
16
|
+
# Join the room
|
17
|
+
# POST /room/#{id}/join.xml
|
18
|
+
# For whatever reason, #join() and #leave() are still xml endpoints
|
19
|
+
# whereas elsewhere in this API we're assuming json :\
|
20
|
+
def join
|
21
|
+
post 'join', 'xml'
|
22
|
+
end
|
23
|
+
|
24
|
+
# Leave a room
|
25
|
+
# POST /room/#{id}/leave.xml
|
26
|
+
def leave
|
27
|
+
post 'leave', 'xml'
|
28
|
+
stop_listening
|
29
|
+
end
|
30
|
+
|
31
|
+
# Get the url for guest access
|
32
|
+
def guest_url
|
33
|
+
"#{@connection.uri}/#{guest_invite_code}" if guest_access_enabled?
|
34
|
+
end
|
35
|
+
|
36
|
+
def guest_access_enabled?
|
37
|
+
load
|
38
|
+
@open_to_guests ? true : false
|
39
|
+
end
|
40
|
+
|
41
|
+
# The invite code use for guest
|
42
|
+
def guest_invite_code
|
43
|
+
load
|
44
|
+
@active_token_value
|
45
|
+
end
|
46
|
+
|
47
|
+
# Change the name of the room
|
48
|
+
def name=(name)
|
49
|
+
update :name => name
|
50
|
+
end
|
51
|
+
alias_method :rename, :name=
|
52
|
+
|
53
|
+
# Change the topic
|
54
|
+
def topic=(topic)
|
55
|
+
update :topic => topic
|
56
|
+
end
|
57
|
+
|
58
|
+
def update(attrs)
|
59
|
+
connection.put("/room/#{@id}.json", {:room => attrs})
|
60
|
+
end
|
61
|
+
|
62
|
+
# Get the current topic
|
63
|
+
def topic
|
64
|
+
reload!
|
65
|
+
@topic
|
66
|
+
end
|
67
|
+
|
68
|
+
# Lock the room to prevent new users from entering and to disable logging
|
69
|
+
def lock
|
70
|
+
post 'lock'
|
71
|
+
end
|
72
|
+
|
73
|
+
# Unlock the room
|
74
|
+
def unlock
|
75
|
+
post 'unlock'
|
76
|
+
end
|
77
|
+
|
78
|
+
# Post a new message to the chat room
|
79
|
+
def speak(message, options = {})
|
80
|
+
send_message(message)
|
81
|
+
end
|
82
|
+
|
83
|
+
def paste(message)
|
84
|
+
send_message(message, 'PasteMessage')
|
85
|
+
end
|
86
|
+
|
87
|
+
def play(sound)
|
88
|
+
send_message(sound, 'SoundMessage')
|
89
|
+
end
|
90
|
+
|
91
|
+
def tweet(url)
|
92
|
+
send_message(url, 'TweetMessage')
|
93
|
+
end
|
94
|
+
|
95
|
+
# Get the list of users currently chatting for this room
|
96
|
+
def users
|
97
|
+
@users ||= current_users
|
98
|
+
end
|
99
|
+
|
100
|
+
def current_users
|
101
|
+
reload!
|
102
|
+
@current_users
|
103
|
+
end
|
104
|
+
|
105
|
+
# return the user with the given id; if it isn't in our room cache,
|
106
|
+
# do a request to get it
|
107
|
+
def user(id)
|
108
|
+
if id
|
109
|
+
cached_user = users.detect {|u| u[:id] == id }
|
110
|
+
user = cached_user || fetch_user(id)
|
111
|
+
self.users << user
|
112
|
+
user
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# Perform a request for the user with the given ID
|
117
|
+
def fetch_user(id)
|
118
|
+
user_data = connection.get("/users/#{id}.json")
|
119
|
+
user = user_data && user_data[:user]
|
120
|
+
user[:created_at] = Time.parse(user[:created_at])
|
121
|
+
user
|
122
|
+
end
|
123
|
+
|
124
|
+
# Modifies a hash representation of a Campfire message. Expands +:user_id+
|
125
|
+
# to a full hash at +:user+, generates Timestamp from +:created_at+.
|
126
|
+
#
|
127
|
+
# Full returned hash:
|
128
|
+
# * +:body+: the body of the message
|
129
|
+
# * +:user+: Campfire user, which is itself a hash, of:
|
130
|
+
# * +:id+: User id
|
131
|
+
# * +:name+: User name
|
132
|
+
# * +:email_address+: Email address
|
133
|
+
# * +:admin+: Boolean admin flag
|
134
|
+
# * +:created_at+: User creation timestamp
|
135
|
+
# * +:type+: User type (e.g. Member)
|
136
|
+
# * +:id+: Campfire message id
|
137
|
+
# * +:type+: Campfire message type
|
138
|
+
# * +:room_id+: Campfire room id
|
139
|
+
# * +:created_at+: Message creation timestamp
|
140
|
+
def parse_message(message)
|
141
|
+
message[:user] = user(message.delete(:user_id))
|
142
|
+
message[:created_at] = Time.parse(message[:created_at])
|
143
|
+
message
|
144
|
+
end
|
145
|
+
|
146
|
+
# Listen for new messages in the room, parsing them with #parse_message
|
147
|
+
# and then yielding them to the provided block as they arrive.
|
148
|
+
#
|
149
|
+
# room.listen do |m|
|
150
|
+
# room.speak "Go away!" if m[:body] =~ /Java/i
|
151
|
+
# end
|
152
|
+
def listen(options = {})
|
153
|
+
raise ArgumentError, "no block provided" unless block_given?
|
154
|
+
|
155
|
+
Tinder.logger.info "Joining #{@name}…"
|
156
|
+
join # you have to be in the room to listen
|
157
|
+
|
158
|
+
require 'json'
|
159
|
+
require 'hashie'
|
160
|
+
require 'multi_json'
|
161
|
+
require 'twitter/json_stream'
|
162
|
+
|
163
|
+
auth = connection.basic_auth_settings
|
164
|
+
options = {
|
165
|
+
:host => "streaming.#{Connection::HOST}",
|
166
|
+
:path => room_url_for('live'),
|
167
|
+
:auth => "#{auth[:username]}:#{auth[:password]}",
|
168
|
+
:timeout => 6,
|
169
|
+
:ssl => connection.options[:ssl]
|
170
|
+
}.merge(options)
|
171
|
+
|
172
|
+
Tinder.logger.info "Starting EventMachine server…"
|
173
|
+
EventMachine::run do
|
174
|
+
@stream = Twitter::JSONStream.connect(options)
|
175
|
+
Tinder.logger.info "Listening to #{@name}…"
|
176
|
+
@stream.each_item do |message|
|
177
|
+
message = Hashie::Mash.new(MultiJson.decode(message))
|
178
|
+
message = parse_message(message)
|
179
|
+
yield(message)
|
180
|
+
end
|
181
|
+
|
182
|
+
@stream.on_error do |message|
|
183
|
+
raise ListenFailed.new("got an error! #{message.inspect}!")
|
184
|
+
end
|
185
|
+
|
186
|
+
@stream.on_max_reconnects do |timeout, retries|
|
187
|
+
raise ListenFailed.new("Tried #{retries} times to connect. Got disconnected from #{@name}!")
|
188
|
+
end
|
189
|
+
|
190
|
+
# if we really get disconnected
|
191
|
+
raise ListenFailed.new("got disconnected from #{@name}!") if !EventMachine.reactor_running?
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
def listening?
|
196
|
+
@stream != nil
|
197
|
+
end
|
198
|
+
|
199
|
+
def stop_listening
|
200
|
+
return unless listening?
|
201
|
+
|
202
|
+
Tinder.logger.info "Stopped listening to #{@name}…"
|
203
|
+
@stream.stop
|
204
|
+
@stream = nil
|
205
|
+
end
|
206
|
+
|
207
|
+
# Get the transcript for the given date (returns an array of messages parsed
|
208
|
+
# via #parse_message, see #parse_message for format of returned message)
|
209
|
+
#
|
210
|
+
def transcript(transcript_date = Date.today)
|
211
|
+
unless transcript_date.is_a?(Date)
|
212
|
+
transcript_date = transcript_date.to_date
|
213
|
+
end
|
214
|
+
url = "/room/#{@id}/transcript/#{transcript_date.strftime('%Y/%m/%d')}.json"
|
215
|
+
connection.get(url)['messages'].map do |message|
|
216
|
+
parse_message(message)
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
# Search transcripts for the given term (returns an array of messages parsed
|
221
|
+
# via #parse_message, see #parse_message for format of returned message)
|
222
|
+
#
|
223
|
+
def search(term)
|
224
|
+
encoded_term = URI.encode(term)
|
225
|
+
|
226
|
+
room_messages = connection.get("/search/#{encoded_term}.json")["messages"].select do |message|
|
227
|
+
message[:room_id] == id
|
228
|
+
end
|
229
|
+
|
230
|
+
room_messages.map do |message|
|
231
|
+
parse_message(message)
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
def upload(file, content_type = nil, filename = nil)
|
236
|
+
require 'mime/types'
|
237
|
+
content_type ||= MIME::Types.type_for(filename || file)
|
238
|
+
raw_post(:uploads, { :upload => Faraday::UploadIO.new(file, content_type, filename) })
|
239
|
+
end
|
240
|
+
|
241
|
+
# Get the list of latest files for this room
|
242
|
+
def files(count = 5)
|
243
|
+
get(:uploads)['uploads'].map { |u| u['full_url'] }
|
244
|
+
end
|
245
|
+
|
246
|
+
# Get a list of recent messages
|
247
|
+
# Accepts a hash for options:
|
248
|
+
# * +:limit+: Restrict the number of messages returned
|
249
|
+
# * +:since_message_id+: Get messages created after the specified message id
|
250
|
+
def recent(options = {})
|
251
|
+
options = { :limit => 10, :since_message_id => nil }.merge(options)
|
252
|
+
# Build url manually, faraday has to be 8.0 to do this
|
253
|
+
url = "#{room_url_for(:recent)}?limit=#{options[:limit]}&since_message_id=#{options[:since_message_id]}"
|
254
|
+
|
255
|
+
connection.get(url)['messages'].map do |msg|
|
256
|
+
parse_message(msg)
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
protected
|
261
|
+
|
262
|
+
def load
|
263
|
+
reload! unless @loaded
|
264
|
+
end
|
265
|
+
|
266
|
+
def reload!
|
267
|
+
attributes = connection.get("/room/#{@id}.json")['room']
|
268
|
+
|
269
|
+
@id = attributes['id']
|
270
|
+
@name = attributes['name']
|
271
|
+
@topic = attributes['topic']
|
272
|
+
@full = attributes['full']
|
273
|
+
@open_to_guests = attributes['open_to_guests']
|
274
|
+
@active_token_value = attributes['active_token_value']
|
275
|
+
@current_users = attributes['users'].map do |user|
|
276
|
+
user[:created_at] = Time.parse(user[:created_at])
|
277
|
+
user
|
278
|
+
end
|
279
|
+
|
280
|
+
@loaded = true
|
281
|
+
end
|
282
|
+
|
283
|
+
def send_message(message, type = 'TextMessage')
|
284
|
+
post 'speak', {:message => {:body => message, :type => type}}
|
285
|
+
end
|
286
|
+
|
287
|
+
def get(action)
|
288
|
+
connection.get(room_url_for(action))
|
289
|
+
end
|
290
|
+
|
291
|
+
def post(action, body = nil)
|
292
|
+
connection.post(room_url_for(action), body)
|
293
|
+
end
|
294
|
+
|
295
|
+
def raw_post(action, body = nil)
|
296
|
+
connection.raw_post(room_url_for(action), body)
|
297
|
+
end
|
298
|
+
|
299
|
+
def room_url_for(action, format="json")
|
300
|
+
"/room/#{@id}/#{action}.#{format}"
|
301
|
+
end
|
302
|
+
|
303
|
+
def connection
|
304
|
+
@connection
|
305
|
+
end
|
306
|
+
end
|
307
|
+
end
|
data/site/index.html
ADDED
@@ -0,0 +1,101 @@
|
|
1
|
+
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
2
|
+
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
3
|
+
|
4
|
+
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
5
|
+
<head>
|
6
|
+
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
7
|
+
<title>Tinder</title>
|
8
|
+
<link rel="stylesheet" type="text/css" href="http://opensoul.org/stylesheets/code.css" />
|
9
|
+
<link rel="stylesheet" type="text/css" href="stylesheets/style.css" />
|
10
|
+
<link href="http://opensoul.org/stylesheets/ci.css" rel="stylesheet" type="text/css" />
|
11
|
+
<script src="http://opensoul.org/javascripts/code_highlighter.js" type="text/javascript"></script>
|
12
|
+
<script src="http://opensoul.org/javascripts/ruby.js" type="text/javascript"></script>
|
13
|
+
</head>
|
14
|
+
|
15
|
+
<body>
|
16
|
+
<div id="collectiveidea">
|
17
|
+
<a href="http://collectiveidea.com"><img src="http://opensoul.org/images/header_logo.gif" alt="Collective Idea" class="logo" width="17" height="22" /></a>
|
18
|
+
<ul class="links">
|
19
|
+
<li><a href="http://daniel.collectiveidea.com/blog">Daniel</a></li>
|
20
|
+
<li><a href="http://opensoul.org">Brandon</a></li>
|
21
|
+
<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>
|
22
|
+
</ul>
|
23
|
+
</div>
|
24
|
+
<div id="main">
|
25
|
+
<div id="header">
|
26
|
+
<h1><a href="/">Tinder</a></h1>
|
27
|
+
<p>Getting the campfire started</p>
|
28
|
+
<ul id="nav">
|
29
|
+
<li><a href="tinder">API Docs</a></li>
|
30
|
+
<li><a href="http://rubyforge.org/projects/tinder">RubyForge</a></li>
|
31
|
+
<li><a href="http://opensoul.org/tags/tinder">Blog</a></li>
|
32
|
+
</ul>
|
33
|
+
</div>
|
34
|
+
<div id="content">
|
35
|
+
<p>Tinder is an API for interfacing with <a href="http://campfirenow.com">Campfire</a>, the 37Signals chat application.</p>
|
36
|
+
<h2>Example</h2>
|
37
|
+
|
38
|
+
<pre><code class="ruby">campfire = Tinder::Campfire.new 'mysubdomain'
|
39
|
+
campfire.login 'myemail@example.com', 'mypassword'</code></pre>
|
40
|
+
|
41
|
+
<h3>Create, find and destroy rooms</h3>
|
42
|
+
<pre><code class="ruby">room = campfire.create_room 'New Room', 'My new campfire room to test tinder'
|
43
|
+
room = campfire.find_room_by_name 'Other Room'
|
44
|
+
room.destroy</code></pre>
|
45
|
+
|
46
|
+
<h3>Speak and Paste</h3>
|
47
|
+
<pre><code class="ruby">room.speak 'Hello world!'
|
48
|
+
room.paste File.read("path/to/your/file.txt")</code></pre>
|
49
|
+
|
50
|
+
<h3>Listening</h3>
|
51
|
+
<pre><code class="ruby">room.listen
|
52
|
+
#=> [{:person=>"Brandon", :message=>"I'm getting very sleepy", :user_id=>"148583", :id=>"16434003"}]
|
53
|
+
|
54
|
+
# or in block form
|
55
|
+
room.listen do |m|
|
56
|
+
room.speak 'Welcome!' if m[:message] == /hello/
|
57
|
+
end</code></pre>
|
58
|
+
|
59
|
+
<h3>Guest Access</h3>
|
60
|
+
<pre><code class="ruby">room.toggle_guest_access
|
61
|
+
room.guest_url #=> http://mysubdomain.campfirenow.com/11111
|
62
|
+
room.guest_invite_code #=> 11111</code></pre>
|
63
|
+
|
64
|
+
<h3>Change the name and topic</h3>
|
65
|
+
<pre><code class="ruby">room.name = 'Tinder Demo'
|
66
|
+
room.topic = 'Showing how to change the room name and topic with tinder…'</code></pre>
|
67
|
+
|
68
|
+
<h3>Users</h3>
|
69
|
+
<pre><code class="ruby">room.users
|
70
|
+
campfire.users # users in all rooms</code></pre>
|
71
|
+
|
72
|
+
<h3>Transcripts</h3>
|
73
|
+
<pre><code class="ruby">transcript = room.transcript(room.available_transcripts.first)
|
74
|
+
#=> [{:message=>"foobar!", :user_id=>"99999", :person=>"Brandon", :id=>"18659245", :timestamp=>Tue May 05 07:15:00 -0700 2009}]
|
75
|
+
</code></pre>
|
76
|
+
|
77
|
+
<p>See the <a href="tinder">API documentation</a> for more details.</p>
|
78
|
+
|
79
|
+
<h2>Installation</h2>
|
80
|
+
|
81
|
+
<p>Tinder can be installed as a gem or a Rails plugin. Install the gem by executing:</p>
|
82
|
+
|
83
|
+
<pre>gem install tinder</pre>
|
84
|
+
|
85
|
+
<p>Or, download it from <a href="http://rubyforge.org/frs/?group_id=2922">RubyForge</a>.</p>
|
86
|
+
|
87
|
+
<h2>Source</h2>
|
88
|
+
|
89
|
+
<p>Contributions are welcome and appreciated! The source is available from:</p>
|
90
|
+
|
91
|
+
<pre>http://github.com/collectiveidea/tinder</pre>
|
92
|
+
</div>
|
93
|
+
</div>
|
94
|
+
<script src="http://www.google-analytics.com/urchin.js" type="text/javascript">
|
95
|
+
</script>
|
96
|
+
<script type="text/javascript">
|
97
|
+
_uacct = "UA-194397-8";
|
98
|
+
urchinTracker();
|
99
|
+
</script>
|
100
|
+
</body>
|
101
|
+
</html>
|
@@ -0,0 +1,77 @@
|
|
1
|
+
body {
|
2
|
+
font-family: "Lucida Grande", Helvetica, Arial, sans-serif;
|
3
|
+
font-size: 76%;
|
4
|
+
background: #2A2A2A;
|
5
|
+
margin: 0;
|
6
|
+
padding: 0;
|
7
|
+
}
|
8
|
+
|
9
|
+
#collectiveidea {
|
10
|
+
border-bottom: 1px solid #444;
|
11
|
+
}
|
12
|
+
|
13
|
+
a {
|
14
|
+
color: #2D5385;
|
15
|
+
}
|
16
|
+
|
17
|
+
#main {
|
18
|
+
background-color: #FFF;
|
19
|
+
width: 700px;
|
20
|
+
margin: 0 auto;
|
21
|
+
border: 5px #CCC;
|
22
|
+
border-left-style: solid;
|
23
|
+
border-right-style: solid;
|
24
|
+
padding: 0 1em;
|
25
|
+
}
|
26
|
+
|
27
|
+
#header {
|
28
|
+
position: relative;
|
29
|
+
border-bottom: 1px solid #999;
|
30
|
+
padding: 1em;
|
31
|
+
}
|
32
|
+
|
33
|
+
#header h1 {
|
34
|
+
margin: 0;
|
35
|
+
padding: 0;
|
36
|
+
color: #2D5385;
|
37
|
+
}
|
38
|
+
|
39
|
+
#header h1 a {
|
40
|
+
text-decoration: none;
|
41
|
+
}
|
42
|
+
|
43
|
+
#header p {
|
44
|
+
margin: 0;
|
45
|
+
padding: 0;
|
46
|
+
font-size: 0.8em;
|
47
|
+
color: #999;
|
48
|
+
}
|
49
|
+
|
50
|
+
#nav {
|
51
|
+
list-style: none;
|
52
|
+
position: absolute;
|
53
|
+
right: 0;
|
54
|
+
top: 0.6em;
|
55
|
+
}
|
56
|
+
#nav li {
|
57
|
+
display: inline;
|
58
|
+
padding: 0 0.5em;
|
59
|
+
}
|
60
|
+
|
61
|
+
#content {
|
62
|
+
padding: 1em 0;
|
63
|
+
}
|
64
|
+
|
65
|
+
dl {
|
66
|
+
background-color: #DDD;
|
67
|
+
padding: 1em;
|
68
|
+
border: 1px solid #CCC;
|
69
|
+
}
|
70
|
+
dl .pronunciation {
|
71
|
+
color: #C00;
|
72
|
+
}
|
73
|
+
dl .description {
|
74
|
+
text-transform: uppercase;
|
75
|
+
font-size: 0.8em;
|
76
|
+
font-family: fixed;
|
77
|
+
}
|