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 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
- To install:
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: example
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
@@ -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"
@@ -1,3 +1,3 @@
1
1
  module CampfireExport
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
@@ -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
- "campfire/#{CampfireExport::Account.subdomain}/#{room.name}/" +
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
- "(expected: #{File.expand_path(export_dir)}, actual: #{true_path}).")
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
- begin
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 does not exist")
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
- puts "*** Error: #{message}"
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 "#{message}\n"
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
- STDOUT.flush
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
- settings_html = Nokogiri::HTML get('/account/settings').body
147
- selected_zone = settings_html.css('select[id="account_time_zone_id"] ' +
148
- '> option[selected="selected"]')
149
- find_tzinfo(selected_zone.attribute("value").text)
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 CampfireExport::Exception => e
160
- log(:error, "room list download failed: #{e}")
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
- last_message = Nokogiri::XML get("/room/#{id}/recent.xml?limit=1").body
177
- update_utc = DateTime.parse(last_message.css('created-at').text)
178
- @last_update = CampfireExport::Account.timezone.utc_to_local(update_utc)
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
- transcript_xml = Nokogiri::XML get("#{transcript_path}.xml").body
216
-
217
- @messages = transcript_xml.css('message').map do |message|
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
- FileUtils.mkdir_p export_dir
225
-
226
- export_file(transcript_xml, 'transcript.xml')
227
- verify_export('transcript.xml', transcript_xml.to_s.length)
228
-
229
- export_plaintext
230
- export_html
231
- export_uploads
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 CampfireExport::Exception => e
249
- log(:error, "Plaintext transcript export for #{export_dir} failed: #{e}")
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
- # Make the upload links in the transcript clickable for the exported
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{<a href="/room/#{room.id}/uploads/},
259
- %Q{<a href="uploads/})
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 CampfireExport::Exception => e
263
- log(:error, "HTML transcript export for #{export_dir} failed: #{e}")
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 CampfireExport::Exception => e
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
- begin
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
- begin
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, :content, :byte_size
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
- content_path = "/room/#{room.id}/uploads/#{id}/#{escaped_name}"
408
- @content = get(content_path).body
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: 29
4
+ hash: 27
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
8
  - 0
9
- - 1
10
- version: 0.0.1
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: 3
117
+ hash: 57
118
118
  segments:
119
- - 0
120
- version: "0"
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: