campfire_export 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +19 -11
- data/campfire_export.gemspec +2 -0
- data/lib/campfire_export/version.rb +1 -1
- data/lib/campfire_export.rb +125 -83
- metadata +10 -8
data/README.md
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
## Quick Start ##
|
4
4
|
|
5
|
-
$ gem install campfire_export
|
5
|
+
$ sudo gem install campfire_export
|
6
6
|
$ campfire_export
|
7
7
|
|
8
8
|
## Intro ##
|
@@ -18,7 +18,7 @@ I found a [Gist](https://gist.github.com) that looked pretty good:
|
|
18
18
|
* [https://gist.github.com/821553](https://gist.github.com/821553)
|
19
19
|
|
20
20
|
but it wasn't quite right. So this is my modification, converted to a GitHub
|
21
|
-
repo.
|
21
|
+
repo and a [Ruby gem](http://docs.rubygems.org/read/chapter/1).
|
22
22
|
|
23
23
|
## Features ##
|
24
24
|
|
@@ -29,11 +29,13 @@ repo.
|
|
29
29
|
|
30
30
|
## Installing ##
|
31
31
|
|
32
|
-
Ruby 1.8.7 or later is required.
|
32
|
+
[Ruby 1.8.7](http://www.ruby-lang.org/en/downloads/) or later is required.
|
33
|
+
[RubyGems](https://rubygems.org/pages/download) is also required -- I'd
|
34
|
+
recommend having the latest version of RubyGems installed before starting.
|
33
35
|
|
34
|
-
|
36
|
+
Once you are set up, to install, run the following:
|
35
37
|
|
36
|
-
$ gem install campfire_export
|
38
|
+
$ sudo gem install campfire_export
|
37
39
|
|
38
40
|
## Configuring ##
|
39
41
|
|
@@ -44,7 +46,7 @@ the export, you can create a `.campfire_export.yaml` file in your home
|
|
44
46
|
directory using this template:
|
45
47
|
|
46
48
|
# Your Campfire subdomain (for 'https://myco.campfirenow.com', use 'myco').
|
47
|
-
subdomain:
|
49
|
+
subdomain: myco
|
48
50
|
|
49
51
|
# Your Campfire API token (see "My Info" on your Campfire site).
|
50
52
|
api_token: abababababababababababababababababababab
|
@@ -82,6 +84,17 @@ structure will be sparse (no messages == no directory).
|
|
82
84
|
|
83
85
|
## Credit ##
|
84
86
|
|
87
|
+
First, thanks a ton to [Jeffrey Hardy](https://github.com/packagethief) from
|
88
|
+
37signals, who helped me track down some bugs in my code as well as some
|
89
|
+
confusion in what I was getting back from Campfire. His patient and determined
|
90
|
+
help made it possible to get this working. Thanks, Jeff!
|
91
|
+
|
92
|
+
Also, thanks much for all the help, comments and contributions:
|
93
|
+
|
94
|
+
* [Brad Greenlee](https://github.com/bgreenlee)
|
95
|
+
* [Andre Arko](https://github.com/indirect)
|
96
|
+
* [Brian Donovan](https://github.com/eventualbuddha)
|
97
|
+
|
85
98
|
As mentioned above, some of the work on this was done by other people. The
|
86
99
|
Gist I forked had contributions from:
|
87
100
|
|
@@ -89,11 +102,6 @@ Gist I forked had contributions from:
|
|
89
102
|
* [Bruno Mattarollo](https://github.com/bruno)
|
90
103
|
* [bf4](https://github.com/bf4)
|
91
104
|
|
92
|
-
Also, thanks much for all the help, comments and contributions:
|
93
|
-
|
94
|
-
* [Jeffrey Hardy](https://github.com/packagethief)
|
95
|
-
* [Brad Greenlee](https://github.com/bgreenlee)
|
96
|
-
|
97
105
|
Thanks, all!
|
98
106
|
|
99
107
|
- Marc Hedlund, marc@precipice.org
|
data/campfire_export.gemspec
CHANGED
@@ -8,11 +8,13 @@ Gem::Specification.new do |s|
|
|
8
8
|
s.platform = Gem::Platform::RUBY
|
9
9
|
s.authors = ["Marc Hedlund"]
|
10
10
|
s.email = ["marc@precipice.org"]
|
11
|
+
s.license = "Apache 2.0"
|
11
12
|
s.homepage = "https://github.com/precipice/campfire_export"
|
12
13
|
s.summary = %q{Export transcripts and uploaded files from your 37signals' Campfire account.}
|
13
14
|
s.description = s.summary
|
14
15
|
|
15
16
|
s.rubyforge_project = "campfire_export"
|
17
|
+
s.required_ruby_version = '>= 1.8.7'
|
16
18
|
|
17
19
|
s.add_development_dependency "bundler", "~> 1.0.15"
|
18
20
|
s.add_development_dependency "tzinfo", "~> 0.3.29"
|
data/lib/campfire_export.rb
CHANGED
@@ -51,10 +51,14 @@ module CampfireExport
|
|
51
51
|
def zero_pad(number)
|
52
52
|
"%02d" % number
|
53
53
|
end
|
54
|
-
|
54
|
+
|
55
|
+
def account_dir
|
56
|
+
"campfire/#{CampfireExport::Account.subdomain}"
|
57
|
+
end
|
58
|
+
|
55
59
|
# Requires that room and date be defined in the calling object.
|
56
60
|
def export_dir
|
57
|
-
"
|
61
|
+
"#{account_dir}/#{room.name}/" +
|
58
62
|
"#{date.year}/#{zero_pad(date.mon)}/#{zero_pad(date.day)}"
|
59
63
|
end
|
60
64
|
|
@@ -64,20 +68,15 @@ module CampfireExport
|
|
64
68
|
true_path = File.expand_path(File.join(export_dir, filename))
|
65
69
|
unless true_path.start_with?(File.expand_path(export_dir))
|
66
70
|
raise CampfireExport::Exception.new("#{export_dir}/#{filename}",
|
67
|
-
"can't export file to a directory higher than target directory " +
|
68
|
-
"
|
71
|
+
"can't export file to a directory higher than target directory; " +
|
72
|
+
"expected: #{File.expand_path(export_dir)}, actual: #{true_path}.")
|
69
73
|
end
|
70
74
|
|
71
75
|
if File.exists?("#{export_dir}/#{filename}")
|
72
|
-
log(:error, "#{export_dir}/#{filename} failed: file already exists
|
76
|
+
log(:error, "#{export_dir}/#{filename} failed: file already exists")
|
73
77
|
else
|
74
78
|
open("#{export_dir}/#{filename}", mode) do |file|
|
75
|
-
|
76
|
-
file.write content
|
77
|
-
rescue => e
|
78
|
-
log(:error, "#{export_dir}/#{filename} failed: " +
|
79
|
-
"#{e.backtrace.join("\n")}")
|
80
|
-
end
|
79
|
+
file.write content
|
81
80
|
end
|
82
81
|
end
|
83
82
|
end
|
@@ -86,7 +85,7 @@ module CampfireExport
|
|
86
85
|
full_path = "#{export_dir}/#{filename}"
|
87
86
|
unless File.exists?(full_path)
|
88
87
|
raise CampfireExport::Exception.new(full_path,
|
89
|
-
"file should have been exported but
|
88
|
+
"file should have been exported but did not make it to disk")
|
90
89
|
end
|
91
90
|
unless File.size(full_path) == expected_size
|
92
91
|
raise CampfireExport::Exception.new(full_path,
|
@@ -95,16 +94,21 @@ module CampfireExport
|
|
95
94
|
end
|
96
95
|
end
|
97
96
|
|
98
|
-
def log(level, message)
|
97
|
+
def log(level, message, exception=nil)
|
99
98
|
case level
|
100
99
|
when :error
|
101
|
-
|
100
|
+
short_error = ["*** Error: #{message}", exception].compact.join(": ")
|
101
|
+
$stderr.puts short_error
|
102
102
|
open("campfire/export_errors.txt", 'a') do |log|
|
103
|
-
log.write
|
103
|
+
log.write short_error
|
104
|
+
unless exception.nil?
|
105
|
+
log.write %Q{\n\t#{exception.backtrace.join("\n\t")}}
|
106
|
+
end
|
107
|
+
log.write "\n"
|
104
108
|
end
|
105
109
|
else
|
106
110
|
print message
|
107
|
-
|
111
|
+
$stdout.flush
|
108
112
|
end
|
109
113
|
end
|
110
114
|
end
|
@@ -139,14 +143,23 @@ module CampfireExport
|
|
139
143
|
CampfireExport::Account.subdomain = subdomain
|
140
144
|
CampfireExport::Account.api_token = api_token
|
141
145
|
CampfireExport::Account.base_url = "https://#{subdomain}.campfirenow.com"
|
146
|
+
|
147
|
+
# Make the base directory immediately so we can start logging errors.
|
148
|
+
FileUtils.mkdir_p account_dir
|
149
|
+
|
142
150
|
CampfireExport::Account.timezone = parse_timezone
|
143
151
|
end
|
144
152
|
|
145
153
|
def parse_timezone
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
154
|
+
begin
|
155
|
+
settings_html = Nokogiri::HTML get('/account/settings').body
|
156
|
+
selected_zone = settings_html.css('select[id="account_time_zone_id"] ' +
|
157
|
+
'> option[selected="selected"]')
|
158
|
+
find_tzinfo(selected_zone.attribute("value").text)
|
159
|
+
rescue => e
|
160
|
+
log(:error, "couldn't find timezone setting (using GMT instead)", e)
|
161
|
+
find_tzinfo("Etc/GMT")
|
162
|
+
end
|
150
163
|
end
|
151
164
|
|
152
165
|
def export(start_date=nil, end_date=nil)
|
@@ -156,8 +169,8 @@ module CampfireExport
|
|
156
169
|
room = CampfireExport::Room.new(room_xml)
|
157
170
|
room.export(start_date, end_date)
|
158
171
|
end
|
159
|
-
rescue
|
160
|
-
log(:error, "room list download failed
|
172
|
+
rescue => e
|
173
|
+
log(:error, "room list download failed", e)
|
161
174
|
end
|
162
175
|
end
|
163
176
|
end
|
@@ -173,10 +186,14 @@ module CampfireExport
|
|
173
186
|
created_utc = DateTime.parse(room_xml.css('created-at').text)
|
174
187
|
@created_at = CampfireExport::Account.timezone.utc_to_local(created_utc)
|
175
188
|
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
189
|
+
begin
|
190
|
+
last_message = Nokogiri::XML get("/room/#{id}/recent.xml?limit=1").body
|
191
|
+
update_utc = DateTime.parse(last_message.css('created-at').text)
|
192
|
+
@last_update = CampfireExport::Account.timezone.utc_to_local(update_utc)
|
193
|
+
rescue => e
|
194
|
+
log(:error, "couldn't get last update in #{room} (defaulting to today)", e)
|
195
|
+
@last_update = Time.now
|
196
|
+
end
|
180
197
|
end
|
181
198
|
|
182
199
|
def export(start_date=nil, end_date=nil)
|
@@ -198,7 +215,7 @@ module CampfireExport
|
|
198
215
|
|
199
216
|
class Transcript
|
200
217
|
include CampfireExport::IO
|
201
|
-
attr_accessor :room, :date, :messages
|
218
|
+
attr_accessor :room, :date, :xml, :messages
|
202
219
|
|
203
220
|
def initialize(room, date)
|
204
221
|
@room = room
|
@@ -212,55 +229,72 @@ module CampfireExport
|
|
212
229
|
def export
|
213
230
|
begin
|
214
231
|
log(:info, "#{export_dir} ... ")
|
215
|
-
|
216
|
-
|
217
|
-
|
232
|
+
@xml = Nokogiri::XML get("#{transcript_path}.xml").body
|
233
|
+
rescue CampfireExport::Exception => e
|
234
|
+
log(:error, "transcript export for #{export_dir} failed", e)
|
235
|
+
else
|
236
|
+
@messages = xml.css('message').map do |message|
|
218
237
|
CampfireExport::Message.new(message, room, date)
|
219
238
|
end
|
220
|
-
|
239
|
+
|
221
240
|
# Only export transcripts that contain at least one message.
|
222
241
|
if messages.length > 0
|
223
242
|
log(:info, "exporting transcripts\n")
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
243
|
+
begin
|
244
|
+
FileUtils.mkdir_p export_dir
|
245
|
+
rescue => e
|
246
|
+
log(:error, "Unable to create #{export_dir}", e)
|
247
|
+
else
|
248
|
+
export_xml
|
249
|
+
export_plaintext
|
250
|
+
export_html
|
251
|
+
export_uploads
|
252
|
+
end
|
232
253
|
else
|
233
254
|
log(:info, "no messages\n")
|
234
|
-
end
|
235
|
-
rescue CampfireExport::Exception => e
|
236
|
-
log(:error, "transcript export for #{export_dir} failed: #{e}")
|
255
|
+
end
|
237
256
|
end
|
238
257
|
end
|
239
|
-
|
258
|
+
|
259
|
+
def export_xml
|
260
|
+
begin
|
261
|
+
export_file(xml, 'transcript.xml')
|
262
|
+
verify_export('transcript.xml', xml.to_s.length)
|
263
|
+
rescue => e
|
264
|
+
log(:error, "XML transcript export for #{export_dir} failed", e)
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
240
268
|
def export_plaintext
|
241
269
|
begin
|
270
|
+
date_header = date.strftime('%A, %B %e, %Y').squeeze(" ")
|
242
271
|
plaintext = "#{CampfireExport::Account.subdomain.upcase} CAMPFIRE\n"
|
243
|
-
plaintext << "#{room.name}: "
|
244
|
-
"#{date.strftime('%A, %B %e, %Y').gsub(' ', ' ')}\n\n"
|
272
|
+
plaintext << "#{room.name}: #{date_header}\n\n"
|
245
273
|
messages.each {|message| plaintext << message.to_s }
|
246
274
|
export_file(plaintext, 'transcript.txt')
|
247
275
|
verify_export('transcript.txt', plaintext.length)
|
248
|
-
rescue
|
249
|
-
log(:error, "Plaintext transcript export for #{export_dir} failed
|
276
|
+
rescue => e
|
277
|
+
log(:error, "Plaintext transcript export for #{export_dir} failed", e)
|
250
278
|
end
|
251
279
|
end
|
252
280
|
|
253
281
|
def export_html
|
254
282
|
begin
|
255
283
|
transcript_html = get(transcript_path)
|
256
|
-
|
284
|
+
|
285
|
+
# Make the upload links in the transcript clickable from the exported
|
286
|
+
# directory layout.
|
287
|
+
transcript_html.gsub!(%Q{href="/room/#{room.id}/uploads/},
|
288
|
+
%Q{href="uploads/})
|
289
|
+
# Likewise, make the image thumbnails embeddable from the exported
|
257
290
|
# directory layout.
|
258
|
-
transcript_html.gsub!(%Q{
|
259
|
-
%Q{
|
291
|
+
transcript_html.gsub!(%Q{src="/room/#{room.id}/thumb/},
|
292
|
+
%Q{src="thumbs/})
|
293
|
+
|
260
294
|
export_file(transcript_html, 'transcript.html')
|
261
295
|
verify_export('transcript.html', transcript_html.length)
|
262
|
-
rescue
|
263
|
-
log(:error, "HTML transcript export for #{export_dir} failed
|
296
|
+
rescue => e
|
297
|
+
log(:error, "HTML transcript export for #{export_dir} failed", e)
|
264
298
|
end
|
265
299
|
end
|
266
300
|
|
@@ -269,10 +303,9 @@ module CampfireExport
|
|
269
303
|
if message.is_upload?
|
270
304
|
begin
|
271
305
|
message.upload.export
|
272
|
-
rescue
|
306
|
+
rescue => e
|
273
307
|
path = "#{message.upload.export_dir}/#{message.upload.filename}"
|
274
|
-
log(:error, "Upload export for #{path} failed
|
275
|
-
"#{e.backtrace.join("\n")}")
|
308
|
+
log(:error, "Upload export for #{path} failed", e)
|
276
309
|
end
|
277
310
|
end
|
278
311
|
end
|
@@ -296,24 +329,19 @@ module CampfireExport
|
|
296
329
|
|
297
330
|
no_user = ['TimestampMessage', 'SystemMessage', 'AdvertisementMessage']
|
298
331
|
unless no_user.include?(@type)
|
299
|
-
|
300
|
-
@user = username(message.css('user-id').text)
|
301
|
-
rescue CampfireExport::Exception
|
302
|
-
@user = "[unknown user]"
|
303
|
-
end
|
332
|
+
@user = username(message.css('user-id').text)
|
304
333
|
end
|
305
334
|
|
306
|
-
|
307
|
-
@upload = CampfireExport::Upload.new(self) if is_upload?
|
308
|
-
rescue e
|
309
|
-
log(:error, "Got an exception while making an upload: #{e}")
|
310
|
-
end
|
335
|
+
@upload = CampfireExport::Upload.new(self) if is_upload?
|
311
336
|
end
|
312
337
|
|
313
338
|
def username(user_id)
|
314
339
|
@@usernames ||= {}
|
315
340
|
@@usernames[user_id] ||= begin
|
316
341
|
doc = Nokogiri::XML get("/users/#{user_id}.xml").body
|
342
|
+
rescue
|
343
|
+
"[unknown user]"
|
344
|
+
else
|
317
345
|
doc.css('name').text
|
318
346
|
end
|
319
347
|
end
|
@@ -373,7 +401,7 @@ module CampfireExport
|
|
373
401
|
|
374
402
|
class Upload
|
375
403
|
include CampfireExport::IO
|
376
|
-
attr_accessor :message, :room, :date, :id, :filename, :
|
404
|
+
attr_accessor :message, :room, :date, :id, :filename, :content_type, :byte_size
|
377
405
|
|
378
406
|
def initialize(message)
|
379
407
|
@message = message
|
@@ -386,10 +414,19 @@ module CampfireExport
|
|
386
414
|
@deleted
|
387
415
|
end
|
388
416
|
|
417
|
+
def is_image?
|
418
|
+
content_type.start_with?("image/")
|
419
|
+
end
|
420
|
+
|
389
421
|
def upload_dir
|
390
422
|
"uploads/#{id}"
|
391
423
|
end
|
392
424
|
|
425
|
+
# Image thumbnails are used to inline image uploads in HTML transcripts.
|
426
|
+
def thumb_dir
|
427
|
+
"thumbs/#{id}"
|
428
|
+
end
|
429
|
+
|
393
430
|
def export
|
394
431
|
begin
|
395
432
|
log(:info, " #{message.body} ... ")
|
@@ -401,21 +438,12 @@ module CampfireExport
|
|
401
438
|
# Get the upload itself and export it.
|
402
439
|
@id = upload.css('id').text
|
403
440
|
@byte_size = upload.css('byte-size').text.to_i
|
441
|
+
@content_type = upload.css('content-type').text
|
404
442
|
@filename = upload.css('name').text
|
405
|
-
escaped_name = CGI.escape(filename)
|
406
443
|
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
# Write uploads to a subdirectory, using the upload ID as a directory
|
411
|
-
# name to avoid overwriting multiple uploads of the same file within
|
412
|
-
# the same day (for instance, if 'Picture 1.png' is uploaded twice
|
413
|
-
# in a day, this will preserve both copies). This path pattern also
|
414
|
-
# matches the tail of the upload path in the HTML transcript, making
|
415
|
-
# it easier to make downloads functional from the HTML transcripts.
|
416
|
-
FileUtils.mkdir_p "#{export_dir}/#{upload_dir}"
|
417
|
-
export_file(content, "#{upload_dir}/#{filename}", 'wb')
|
418
|
-
verify_export("#{upload_dir}/#{filename}", byte_size)
|
444
|
+
export_content(upload_dir)
|
445
|
+
export_content(thumb_dir, path_component="thumb/#{id}", verify=false) if is_image?
|
446
|
+
|
419
447
|
log(:info, "ok\n")
|
420
448
|
rescue CampfireExport::Exception => e
|
421
449
|
if e.code == 404
|
@@ -423,13 +451,27 @@ module CampfireExport
|
|
423
451
|
@deleted = true
|
424
452
|
log(:info, "deleted\n")
|
425
453
|
else
|
426
|
-
log(:error, "Got an upload error: #{e.backtrace.join("\n")}")
|
427
454
|
raise e
|
428
455
|
end
|
429
|
-
rescue => e
|
430
|
-
log(:error, "export of #{export_dir}/#{upload_dir}/#{filename} failed:\n" +
|
431
|
-
"#{e}:\n#{e.backtrace.join("\n")}")
|
432
456
|
end
|
433
457
|
end
|
458
|
+
|
459
|
+
def export_content(content_dir, path_component=nil, verify=true)
|
460
|
+
# If the export directory name is different than the URL path component,
|
461
|
+
# the caller can define the path_component separately.
|
462
|
+
path_component ||= content_dir
|
463
|
+
|
464
|
+
# Write uploads to a subdirectory, using the upload ID as a directory
|
465
|
+
# name to avoid overwriting multiple uploads of the same file within
|
466
|
+
# the same day (for instance, if 'Picture 1.png' is uploaded twice
|
467
|
+
# in a day, this will preserve both copies). This path pattern also
|
468
|
+
# matches the tail of the upload path in the HTML transcript, making
|
469
|
+
# it easier to make downloads functional from the HTML transcripts.
|
470
|
+
content_path = "/room/#{room.id}/#{path_component}/#{CGI.escape(filename)}"
|
471
|
+
content = get(content_path).body
|
472
|
+
FileUtils.mkdir_p(File.join(export_dir, content_dir))
|
473
|
+
export_file(content, "#{content_dir}/#{filename}", 'wb')
|
474
|
+
verify_export("#{content_dir}/#{filename}", byte_size) if verify
|
475
|
+
end
|
434
476
|
end
|
435
477
|
end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: campfire_export
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 27
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 0
|
9
|
-
-
|
10
|
-
version: 0.0.
|
9
|
+
- 2
|
10
|
+
version: 0.0.2
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Marc Hedlund
|
@@ -102,8 +102,8 @@ files:
|
|
102
102
|
- lib/campfire_export/timezone.rb
|
103
103
|
- lib/campfire_export/version.rb
|
104
104
|
homepage: https://github.com/precipice/campfire_export
|
105
|
-
licenses:
|
106
|
-
|
105
|
+
licenses:
|
106
|
+
- Apache 2.0
|
107
107
|
post_install_message:
|
108
108
|
rdoc_options: []
|
109
109
|
|
@@ -114,10 +114,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
114
114
|
requirements:
|
115
115
|
- - ">="
|
116
116
|
- !ruby/object:Gem::Version
|
117
|
-
hash:
|
117
|
+
hash: 57
|
118
118
|
segments:
|
119
|
-
-
|
120
|
-
|
119
|
+
- 1
|
120
|
+
- 8
|
121
|
+
- 7
|
122
|
+
version: 1.8.7
|
121
123
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
122
124
|
none: false
|
123
125
|
requirements:
|