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 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: