campfire_export 0.0.1 → 0.0.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/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:
|