stackbuilders-campfire_export 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,78 @@
1
+ module CampfireExport
2
+ class Upload
3
+ include CampfireExport::IO
4
+ attr_accessor :message, :room, :date, :id, :filename, :content_type, :byte_size, :full_url
5
+
6
+ def initialize(message)
7
+ @message = message
8
+ @room = message.room
9
+ @date = message.date
10
+ @deleted = false
11
+ end
12
+
13
+ def deleted?
14
+ @deleted
15
+ end
16
+
17
+ def is_image?
18
+ content_type.start_with?("image/")
19
+ end
20
+
21
+ def upload_dir
22
+ "uploads/#{id}"
23
+ end
24
+
25
+ # Image thumbnails are used to inline image uploads in HTML transcripts.
26
+ def thumb_dir
27
+ "thumbs/#{id}"
28
+ end
29
+
30
+ def export
31
+ begin
32
+ log(:info, " #{message.body} ... ")
33
+
34
+ # Get the upload object corresponding to this message.
35
+ upload_path = "/room/#{room.id}/messages/#{message.id}/upload.xml"
36
+ upload = Nokogiri::XML get(upload_path).body
37
+
38
+ # Get the upload itself and export it.
39
+ @id = upload.xpath('/upload/id').text
40
+ @byte_size = upload.xpath('/upload/byte-size').text.to_i
41
+ @content_type = upload.xpath('/upload/content-type').text
42
+ @filename = upload.xpath('/upload/name').text
43
+ @full_url = upload.xpath('/upload/full-url').text
44
+
45
+ export_content(upload_dir)
46
+ export_content(thumb_dir, path_component="thumb/#{id}", verify=false) if is_image?
47
+
48
+ log(:info, "ok\n")
49
+ rescue CampfireExport::Exception => e
50
+ if e.code == 404
51
+ # If the upload 404s, that should mean it was subsequently deleted.
52
+ @deleted = true
53
+ log(:info, "deleted\n")
54
+ else
55
+ raise e
56
+ end
57
+ end
58
+ end
59
+
60
+ def export_content(content_dir, path_component=nil, verify=true)
61
+ # If the export directory name is different than the URL path component,
62
+ # the caller can define the path_component separately.
63
+ path_component ||= content_dir
64
+
65
+ # Write uploads to a subdirectory, using the upload ID as a directory
66
+ # name to avoid overwriting multiple uploads of the same file within
67
+ # the same day (for instance, if 'Picture 1.png' is uploaded twice
68
+ # in a day, this will preserve both copies). This path pattern also
69
+ # matches the tail of the upload path in the HTML transcript, making
70
+ # it easier to make downloads functional from the HTML transcripts.
71
+ content_path = "/room/#{room.id}/#{path_component}/#{CGI.escape(filename)}"
72
+ content = get(content_path).body
73
+ FileUtils.mkdir_p(File.join(export_dir, content_dir))
74
+ export_file(content, "#{content_dir}/#{filename}", 'wb')
75
+ verify_export("#{content_dir}/#{filename}", byte_size) if verify
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,3 @@
1
+ module CampfireExport
2
+ VERSION = "0.4.0"
3
+ end
@@ -0,0 +1,133 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Portions copyright 2011 Marc Hedlund <marc@precipice.org>.
4
+ # Adapted from https://gist.github.com/821553 and ancestors.
5
+
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+
18
+ # campfire_export.rb -- export Campfire transcripts and uploaded files.
19
+ #
20
+ # Since Campfire (www.campfirenow.com) doesn't provide an export feature,
21
+ # this script implements one via the Campfire API.
22
+
23
+ require 'rubygems'
24
+
25
+ require 'cgi'
26
+ require 'fileutils'
27
+ require 'httparty'
28
+ require 'nokogiri'
29
+ require 'retryable'
30
+ require 'time'
31
+ require 'yaml'
32
+
33
+ module CampfireExport
34
+ module IO
35
+ MAX_RETRIES = 5
36
+
37
+ def api_url(path)
38
+ "#{CampfireExport::Account.base_url}#{path}"
39
+ end
40
+
41
+ def get(path, params = {})
42
+ url = api_url(path)
43
+
44
+ response = Retryable.retryable(:tries => MAX_RETRIES) do |retries, exception|
45
+ if retries > 0
46
+ msg = "Attempt ##{retries} to fetch #{path} failed, " +
47
+ "#{MAX_RETRIES - retries} attempts remaining"
48
+ log :error, msg, exception
49
+ end
50
+
51
+ HTTParty.get(url, :query => params, :basic_auth =>
52
+ {:username => CampfireExport::Account.api_token, :password => 'X'})
53
+ end
54
+
55
+ if response.code >= 400
56
+ raise CampfireExport::Exception.new(url, response.message, response.code)
57
+ end
58
+ response
59
+ end
60
+
61
+ def zero_pad(number)
62
+ "%02d" % number
63
+ end
64
+
65
+ # Requires that room and date be defined in the calling object.
66
+ def export_dir
67
+ "campfire/#{Account.subdomain}/#{room.name}/" +
68
+ "#{date.year}/#{zero_pad(date.mon)}/#{zero_pad(date.day)}"
69
+ end
70
+
71
+ # Requires that room_name and date be defined in the calling object.
72
+ def export_file(content, filename, mode='w')
73
+ # Check to make sure we're writing into the target directory tree.
74
+ true_path = File.expand_path(File.join(export_dir, filename))
75
+
76
+ unless true_path.start_with?(File.expand_path(export_dir))
77
+ raise CampfireExport::Exception.new("#{export_dir}/#{filename}",
78
+ "can't export file to a directory higher than target directory; " +
79
+ "expected: #{File.expand_path(export_dir)}, actual: #{true_path}.")
80
+ end
81
+
82
+ if File.exists?("#{export_dir}/#{filename}")
83
+ log(:error, "#{export_dir}/#{filename} failed: file already exists")
84
+ else
85
+ open("#{export_dir}/#{filename}", mode) do |file|
86
+ file.write content
87
+ end
88
+ end
89
+ end
90
+
91
+ def verify_export(filename, expected_size)
92
+ full_path = "#{export_dir}/#{filename}"
93
+ unless File.exists?(full_path)
94
+ raise CampfireExport::Exception.new(full_path,
95
+ "file should have been exported but did not make it to disk")
96
+ end
97
+ unless File.size(full_path) == expected_size
98
+ raise CampfireExport::Exception.new(full_path,
99
+ "exported file exists but is not the right size " +
100
+ "(expected: #{expected_size}, actual: #{File.size(full_path)})")
101
+ end
102
+ end
103
+
104
+ def log(level, message, exception=nil)
105
+ case level
106
+ when :error
107
+ short_error = ["*** Error: #{message}", exception].compact.join(": ")
108
+ $stderr.puts short_error
109
+ open("campfire/export_errors.txt", 'a') do |log|
110
+ log.write short_error
111
+ unless exception.nil?
112
+ log.write %Q{\n\t#{exception.backtrace.join("\n\t")}}
113
+ end
114
+ log.write "\n"
115
+ end
116
+ else
117
+ print message
118
+ $stdout.flush
119
+ end
120
+ end
121
+ end
122
+
123
+ end
124
+
125
+ require 'campfire_export/timezone'
126
+
127
+ require 'campfire_export/account'
128
+ require 'campfire_export/exception'
129
+ require 'campfire_export/message'
130
+ require 'campfire_export/room'
131
+ require 'campfire_export/transcript'
132
+ require 'campfire_export/upload'
133
+ require 'campfire_export/version'
@@ -0,0 +1,85 @@
1
+ require 'spec_helper'
2
+
3
+ module CampfireExport
4
+ describe Account do
5
+ before(:each) do
6
+ @subdomain = "test-subdomain"
7
+ @api_token = "test-apikey"
8
+ @account = Account.new(@subdomain, @api_token)
9
+
10
+ @good_timezone = '<?xml version="1.0" encoding="UTF-8"?>' +
11
+ '<account>' +
12
+ ' <time-zone>America/Los_Angeles</time-zone>' +
13
+ ' <owner-id type="integer">99999</owner-id>' +
14
+ ' <created-at type="datetime">2010-01-31T18:30:18Z</created-at>' +
15
+ ' <storage type="integer">9999999</storage>' +
16
+ ' <plan>basic</plan>' +
17
+ ' <updated-at type="datetime">2010-01-31T18:31:55Z</updated-at>' +
18
+ ' <subdomain>example</subdomain>' +
19
+ ' <name>Example</name>' +
20
+ ' <id type="integer">999999</id>' +
21
+ '</account>'
22
+
23
+ @bad_timezone = @good_timezone.gsub('America/Los_Angeles',
24
+ 'No Such Timezone')
25
+ @account_xml = stub("Account XML")
26
+ @account_xml.stub(:body).and_return(@good_timezone)
27
+ end
28
+
29
+ context "when it is created" do
30
+ it "sets up the account config variables" do
31
+ Account.subdomain.should equal(@subdomain)
32
+ Account.api_token.should equal(@api_token)
33
+ Account.base_url.should == "https://#{@subdomain}.campfirenow.com"
34
+ end
35
+ end
36
+
37
+ context "when timezone is loaded" do
38
+ it "determines the user's timezone" do
39
+ @account.should_receive(:get).with("/account.xml"
40
+ ).and_return(@account_xml)
41
+ @account.find_timezone
42
+ Account.timezone.to_s.should == "America - Los Angeles"
43
+ end
44
+
45
+ it "raises an error if it gets a bad time zone identifier" do
46
+ @account_xml.stub(:body).and_return(@bad_timezone)
47
+ @account.stub(:get).with("/account.xml"
48
+ ).and_return(@account_xml)
49
+ expect {
50
+ @account.find_timezone
51
+ }.to raise_error(TZInfo::InvalidTimezoneIdentifier)
52
+ end
53
+
54
+ it "raises an error if it can't get the account settings at all" do
55
+ @account.stub(:get).with("/account.xml"
56
+ ).and_raise(CampfireExport::Exception.new("/account/settings",
57
+ "Not Found", 404))
58
+ expect {
59
+ @account.find_timezone
60
+ }.to raise_error(CampfireExport::Exception)
61
+ end
62
+ end
63
+
64
+ context "when rooms are requested" do
65
+ it "returns an array of rooms" do
66
+ room_xml = "<rooms><room>1</room><room>2</room><room>3</room></rooms>"
67
+ room_doc = mock("room doc")
68
+ room_doc.should_receive(:body).and_return(room_xml)
69
+ @account.should_receive(:get).with('/rooms.xml').and_return(room_doc)
70
+ room = mock("room")
71
+ Room.should_receive(:new).exactly(3).times.and_return(room)
72
+ @account.rooms.should have(3).items
73
+ end
74
+
75
+ it "raises an error if it can't get the room list" do
76
+ @account.stub(:get).with('/rooms.xml'
77
+ ).and_raise(CampfireExport::Exception.new('/rooms.xml',
78
+ "Not Found", 404))
79
+ expect {
80
+ @account.rooms
81
+ }.to raise_error(CampfireExport::Exception)
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,60 @@
1
+ require 'spec_helper'
2
+
3
+ module CampfireExport
4
+ describe Message do
5
+ include TimeZone
6
+
7
+ before :each do
8
+ @messages = Nokogiri::XML <<XML
9
+ <messages>
10
+ <message>
11
+ <created-at type="datetime">2012-05-11T17:45:00Z</created-at>
12
+ <id type="integer">111</id>
13
+ <room-id type="integer">222</room-id>
14
+ <user-id type="integer" nil="true"/>
15
+ <body nil="true"/>
16
+ <type>TimestampMessage</type>
17
+ </message>
18
+ <message>
19
+ <created-at type="datetime">2012-05-11T17:47:20Z</created-at>
20
+ <id type="integer">333</id>
21
+ <room-id type="integer">222</room-id>
22
+ <user-id type="integer">555</user-id>
23
+ <body>This is a tweet</body>
24
+ <type>TweetMessage</type>
25
+ <tweet>
26
+ <id>20100487385931234</id>
27
+ <message>This is a tweet</message>
28
+ <author_username>twitter_user</author_username>
29
+ <author_avatar_url>avatar.jpg</author_avatar_url>
30
+ </tweet>
31
+ </message>
32
+ <message>
33
+ <created-at type="datetime">2012-05-11T17:47:23Z</created-at>
34
+ <id type="integer">666</id>
35
+ <room-id type="integer">222</room-id>
36
+ <user-id type="integer">555</user-id>
37
+ <body>Regular message</body>
38
+ <type>TextMessage</type>
39
+ </message>
40
+ </messages>
41
+ XML
42
+ Account.timezone = find_tzinfo("America/Los_Angeles")
43
+ end
44
+
45
+ context "when it is created" do
46
+ it "sets up basic properties" do
47
+ message = Message.new(@messages.xpath('/messages/message[3]')[0], nil, nil)
48
+ message.body.should == "Regular message"
49
+ message.id.should == "666"
50
+ message.timestamp.should == "10:47 AM"
51
+ end
52
+
53
+ it "handles tweets correctly" do
54
+ message = Message.new(@messages.xpath('/messages/message[2]'), nil, nil)
55
+ message.body.should == "This is a tweet"
56
+ message.id.should == "333"
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,27 @@
1
+ require 'spec_helper'
2
+
3
+ module CampfireExport
4
+ describe Room do
5
+ include TimeZone
6
+
7
+ before :each do
8
+ doc = Nokogiri::XML "<room><name>Test Room</name><id>666</id>" +
9
+ "<created-at>2009-11-17T19:41:38Z</created-at></room>"
10
+ @room_xml = doc.xpath('/room')
11
+ Account.timezone = find_tzinfo("America/Los_Angeles")
12
+ end
13
+
14
+ context "when it is created" do
15
+ it "sets up basic properties" do
16
+ room = Room.new(@room_xml)
17
+ room.name.should == "Test Room"
18
+ room.id.should == "666"
19
+ room.created_at.should == DateTime.parse("2009-11-17T11:41:38Z")
20
+ end
21
+ end
22
+
23
+ context "when it finds the last update" do
24
+ it "loads the last update from the most recent message"
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,4 @@
1
+ require 'rspec'
2
+
3
+ require 'campfire_export'
4
+ require 'tzinfo'
metadata ADDED
@@ -0,0 +1,174 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: stackbuilders-campfire_export
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.4.0
5
+ platform: ruby
6
+ authors:
7
+ - Marc Hedlund
8
+ - Justin Leitgeb
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2015-03-18 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: bundler
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: '1'
21
+ type: :development
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - "~>"
26
+ - !ruby/object:Gem::Version
27
+ version: '1'
28
+ - !ruby/object:Gem::Dependency
29
+ name: rake
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - "~>"
33
+ - !ruby/object:Gem::Version
34
+ version: '10'
35
+ type: :development
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: '10'
42
+ - !ruby/object:Gem::Dependency
43
+ name: rspec
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - "~>"
47
+ - !ruby/object:Gem::Version
48
+ version: '2.6'
49
+ type: :development
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - "~>"
54
+ - !ruby/object:Gem::Version
55
+ version: '2.6'
56
+ - !ruby/object:Gem::Dependency
57
+ name: tzinfo
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - "~>"
61
+ - !ruby/object:Gem::Version
62
+ version: '1.2'
63
+ type: :runtime
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - "~>"
68
+ - !ruby/object:Gem::Version
69
+ version: '1.2'
70
+ - !ruby/object:Gem::Dependency
71
+ name: httparty
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - "~>"
75
+ - !ruby/object:Gem::Version
76
+ version: '0.13'
77
+ type: :runtime
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - "~>"
82
+ - !ruby/object:Gem::Version
83
+ version: '0.13'
84
+ - !ruby/object:Gem::Dependency
85
+ name: nokogiri
86
+ requirement: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - "~>"
89
+ - !ruby/object:Gem::Version
90
+ version: '1.6'
91
+ type: :runtime
92
+ prerelease: false
93
+ version_requirements: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - "~>"
96
+ - !ruby/object:Gem::Version
97
+ version: '1.6'
98
+ - !ruby/object:Gem::Dependency
99
+ name: retryable
100
+ requirement: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - "~>"
103
+ - !ruby/object:Gem::Version
104
+ version: '2.0'
105
+ type: :runtime
106
+ prerelease: false
107
+ version_requirements: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - "~>"
110
+ - !ruby/object:Gem::Version
111
+ version: '2.0'
112
+ description: |-
113
+ Exports content from all rooms in a
114
+ 37signals Campfire account. Creates a directory containing transcripts
115
+ and content uploaded to each one of your rooms. Can be configured to
116
+ recognize start and end date of content export.
117
+ email:
118
+ - marc@precipice.org
119
+ - justin@stackbuilders.com
120
+ executables:
121
+ - campfire_export
122
+ extensions: []
123
+ extra_rdoc_files: []
124
+ files:
125
+ - ".gitignore"
126
+ - ".travis.yml"
127
+ - Gemfile
128
+ - LICENSE.txt
129
+ - README.md
130
+ - Rakefile
131
+ - bin/campfire_export
132
+ - campfire_export.gemspec
133
+ - lib/campfire_export.rb
134
+ - lib/campfire_export/account.rb
135
+ - lib/campfire_export/exception.rb
136
+ - lib/campfire_export/message.rb
137
+ - lib/campfire_export/room.rb
138
+ - lib/campfire_export/timezone.rb
139
+ - lib/campfire_export/transcript.rb
140
+ - lib/campfire_export/upload.rb
141
+ - lib/campfire_export/version.rb
142
+ - spec/campfire_export/account_spec.rb
143
+ - spec/campfire_export/message_spec.rb
144
+ - spec/campfire_export/room_spec.rb
145
+ - spec/spec_helper.rb
146
+ homepage: https://github.com/stackbuilders/campfire_export
147
+ licenses:
148
+ - Apache 2.0
149
+ metadata: {}
150
+ post_install_message:
151
+ rdoc_options: []
152
+ require_paths:
153
+ - lib
154
+ required_ruby_version: !ruby/object:Gem::Requirement
155
+ requirements:
156
+ - - ">="
157
+ - !ruby/object:Gem::Version
158
+ version: 1.9.3
159
+ required_rubygems_version: !ruby/object:Gem::Requirement
160
+ requirements:
161
+ - - ">="
162
+ - !ruby/object:Gem::Version
163
+ version: '0'
164
+ requirements: []
165
+ rubyforge_project: campfire_export
166
+ rubygems_version: 2.4.6
167
+ signing_key:
168
+ specification_version: 4
169
+ summary: Export transcripts and uploaded files from your 37signals' Campfire account.
170
+ test_files:
171
+ - spec/campfire_export/account_spec.rb
172
+ - spec/campfire_export/message_spec.rb
173
+ - spec/campfire_export/room_spec.rb
174
+ - spec/spec_helper.rb