campfire_export 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,44 @@
1
+ # Ruby
2
+ *.gem
3
+ *.rbc
4
+ .bundle
5
+ .config
6
+ coverage
7
+ InstalledFiles
8
+ lib/bundler/man
9
+ rdoc
10
+ spec/reports
11
+ test/tmp
12
+ test/version_tmp
13
+ tmp
14
+ Gemfile.lock
15
+ pkg/*
16
+
17
+
18
+ # YARD artifacts
19
+ .yardoc
20
+ _yardoc
21
+ doc/
22
+
23
+ # Emacs
24
+ *~
25
+ \#*\#
26
+ /.emacs.desktop
27
+ /.emacs.desktop.lock
28
+ .elc
29
+ auto-save-list
30
+ tramp
31
+
32
+ # OSX
33
+ .DS_Store
34
+ Icon?
35
+
36
+ # Thumbnails
37
+ ._*
38
+
39
+ # Files that might appear on external disk
40
+ .Spotlight-V100
41
+ .Trashes
42
+
43
+ # project-specific
44
+ campfire
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in campfire_export.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,160 @@
1
+ Apache License
2
+
3
+ Version 2.0, January 2004
4
+
5
+ http://www.apache.org/licenses/
6
+
7
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
8
+
9
+ 1. Definitions.
10
+
11
+ "License" shall mean the terms and conditions for use, reproduction, and
12
+ distribution as defined by Sections 1 through 9 of this document.
13
+
14
+ "Licensor" shall mean the copyright owner or entity authorized by the
15
+ copyright owner that is granting the License.
16
+
17
+ "Legal Entity" shall mean the union of the acting entity and all other
18
+ entities that control, are controlled by, or are under common control with
19
+ that entity. For the purposes of this definition, "control" means (i) the
20
+ power, direct or indirect, to cause the direction or management of such
21
+ entity, whether by contract or otherwise, or (ii) ownership of fifty percent
22
+ (50%) or more of the outstanding shares, or (iii) beneficial ownership of such
23
+ entity.
24
+
25
+ "You" (or "Your") shall mean an individual or Legal Entity exercising
26
+ permissions granted by this License.
27
+
28
+ "Source" form shall mean the preferred form for making modifications,
29
+ including but not limited to software source code, documentation source, and
30
+ configuration files.
31
+
32
+ "Object" form shall mean any form resulting from mechanical transformation or
33
+ translation of a Source form, including but not limited to compiled object
34
+ code, generated documentation, and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or Object form,
37
+ made available under the License, as indicated by a copyright notice that is
38
+ included in or attached to the work (an example is provided in the Appendix
39
+ below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object form, that
42
+ is based on (or derived from) the Work and for which the editorial revisions,
43
+ annotations, elaborations, or other modifications represent, as a whole, an
44
+ original work of authorship. For the purposes of this License, Derivative
45
+ Works shall not include works that remain separable from, or merely link (or
46
+ bind by name) to the interfaces of, the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including the original
49
+ version of the Work and any modifications or additions to that Work or
50
+ Derivative Works thereof, that is intentionally submitted to Licensor for
51
+ inclusion in the Work by the copyright owner or by an individual or Legal
52
+ Entity authorized to submit on behalf of the copyright owner. For the purposes
53
+ of this definition, "submitted" means any form of electronic, verbal, or
54
+ written communication sent to the Licensor or its representatives, including
55
+ but not limited to communication on electronic mailing lists, source code
56
+ control systems, and issue tracking systems that are managed by, or on behalf
57
+ of, the Licensor for the purpose of discussing and improving the Work, but
58
+ excluding communication that is conspicuously marked or otherwise designated
59
+ in writing by the copyright owner as "Not a Contribution."
60
+
61
+ "Contributor" shall mean Licensor and any individual or Legal Entity on behalf
62
+ of whom a Contribution has been received by Licensor and subsequently
63
+ incorporated within the Work.
64
+
65
+ 2. Grant of Copyright License. Subject to the terms and conditions of this
66
+ License, each Contributor hereby grants to You a perpetual, worldwide,
67
+ non-exclusive, no-charge, royalty-free, irrevocable copyright license to
68
+ reproduce, prepare Derivative Works of, publicly display, publicly perform,
69
+ sublicense, and distribute the Work and such Derivative Works in Source or
70
+ Object form.
71
+
72
+ 3. Grant of Patent License. Subject to the terms and conditions of this
73
+ License, each Contributor hereby grants to You a perpetual, worldwide,
74
+ non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this
75
+ section) patent license to make, have made, use, offer to sell, sell, import,
76
+ and otherwise transfer the Work, where such license applies only to those
77
+ patent claims licensable by such Contributor that are necessarily infringed by
78
+ their Contribution(s) alone or by combination of their Contribution(s) with
79
+ the Work to which such Contribution(s) was submitted. If You institute patent
80
+ litigation against any entity (including a cross-claim or counterclaim in a
81
+ lawsuit) alleging that the Work or a Contribution incorporated within the Work
82
+ constitutes direct or contributory patent infringement, then any patent
83
+ licenses granted to You under this License for that Work shall terminate as of
84
+ the date such litigation is filed.
85
+
86
+ 4. Redistribution. You may reproduce and distribute copies of the Work or
87
+ Derivative Works thereof in any medium, with or without modifications, and in
88
+ Source or Object form, provided that You meet the following conditions:
89
+
90
+ You must give any other recipients of the Work or Derivative Works a copy of
91
+ this License; and
92
+
93
+ You must cause any modified files to carry prominent notices stating that You
94
+ changed the files; and
95
+
96
+ You must retain, in the Source form of any Derivative Works that You
97
+ distribute, all copyright, patent, trademark, and attribution notices from the
98
+ Source form of the Work, excluding those notices that do not pertain to any
99
+ part of the Derivative Works; and
100
+
101
+ If the Work includes a "NOTICE" text file as part of its distribution, then
102
+ any Derivative Works that You distribute must include a readable copy of the
103
+ attribution notices contained within such NOTICE file, excluding those notices
104
+ that do not pertain to any part of the Derivative Works, in at least one of
105
+ the following places: within a NOTICE text file distributed as part of the
106
+ Derivative Works; within the Source form or documentation, if provided along
107
+ with the Derivative Works; or, within a display generated by the Derivative
108
+ Works, if and wherever such third-party notices normally appear. The contents
109
+ of the NOTICE file are for informational purposes only and do not modify the
110
+ License. You may add Your own attribution notices within Derivative Works that
111
+ You distribute, alongside or as an addendum to the NOTICE text from the Work,
112
+ provided that such additional attribution notices cannot be construed as
113
+ modifying the License. You may add Your own copyright statement to Your
114
+ modifications and may provide additional or different license terms and
115
+ conditions for use, reproduction, or distribution of Your modifications, or
116
+ for any such Derivative Works as a whole, provided Your use, reproduction, and
117
+ distribution of the Work otherwise complies with the conditions stated in this
118
+ License.
119
+
120
+ 5. Submission of Contributions. Unless You explicitly state otherwise, any
121
+ Contribution intentionally submitted for inclusion in the Work by You to the
122
+ Licensor shall be under the terms and conditions of this License, without any
123
+ additional terms or conditions. Notwithstanding the above, nothing herein
124
+ shall supersede or modify the terms of any separate license agreement you may
125
+ have executed with Licensor regarding such Contributions.
126
+
127
+ 6. Trademarks. This License does not grant permission to use the trade names,
128
+ trademarks, service marks, or product names of the Licensor, except as
129
+ required for reasonable and customary use in describing the origin of the Work
130
+ and reproducing the content of the NOTICE file.
131
+
132
+ 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in
133
+ writing, Licensor provides the Work (and each Contributor provides its
134
+ Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
135
+ KIND, either express or implied, including, without limitation, any warranties
136
+ or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
137
+ PARTICULAR PURPOSE. You are solely responsible for determining the
138
+ appropriateness of using or redistributing the Work and assume any risks
139
+ associated with Your exercise of permissions under this License.
140
+
141
+ 8. Limitation of Liability. In no event and under no legal theory, whether in
142
+ tort (including negligence), contract, or otherwise, unless required by
143
+ applicable law (such as deliberate and grossly negligent acts) or agreed to in
144
+ writing, shall any Contributor be liable to You for damages, including any
145
+ direct, indirect, special, incidental, or consequential damages of any
146
+ character arising as a result of this License or out of the use or inability
147
+ to use the Work (including but not limited to damages for loss of goodwill,
148
+ work stoppage, computer failure or malfunction, or any and all other
149
+ commercial damages or losses), even if such Contributor has been advised of
150
+ the possibility of such damages.
151
+
152
+ 9. Accepting Warranty or Additional Liability. While redistributing the Work
153
+ or Derivative Works thereof, You may choose to offer, and charge a fee for,
154
+ acceptance of support, warranty, indemnity, or other liability obligations
155
+ and/or rights consistent with this License. However, in accepting such
156
+ obligations, You may act only on Your own behalf and on Your sole
157
+ responsibility, not on behalf of any other Contributor, and only if You agree
158
+ to indemnify, defend, and hold each Contributor harmless for any liability
159
+ incurred by, or claims asserted against, such Contributor by reason of your
160
+ accepting any such warranty or additional liability.
data/README.md ADDED
@@ -0,0 +1,99 @@
1
+ # campfire_export #
2
+
3
+ ## Quick Start ##
4
+
5
+ $ gem install campfire_export
6
+ $ campfire_export
7
+
8
+ ## Intro ##
9
+
10
+ I had an old, defunct [Campfire](http://campfirenow.com/) account with five
11
+ years' worth of transcripts in it, some of them hilarious, others just
12
+ memorable. Unfortunately, Campfire doesn't currently have an export function;
13
+ instead it provides pages of individual transcripts. I wanted a script to
14
+ export everything from all five years, using the Campfire API.
15
+
16
+ I found a [Gist](https://gist.github.com) that looked pretty good:
17
+
18
+ * [https://gist.github.com/821553](https://gist.github.com/821553)
19
+
20
+ but it wasn't quite right. So this is my modification, converted to a GitHub
21
+ repo.
22
+
23
+ ## Features ##
24
+
25
+ * Saves HTML, XML, and plaintext versions of chat transcripts.
26
+ * Exports uploaded files to a day-specific subdirectory for easy access.
27
+ * Reports and logs export errors so you know what you're missing.
28
+ * Obsessively confirms that everything was exported correctly.
29
+
30
+ ## Installing ##
31
+
32
+ Ruby 1.8.7 or later is required.
33
+
34
+ To install:
35
+
36
+ $ gem install campfire_export
37
+
38
+ ## Configuring ##
39
+
40
+ There are a number of configuration variables required to run the export. The
41
+ export script will prompt you for these; just run it and away you go. If you
42
+ want to run the script repeatedly or want to control the start and end date of
43
+ the export, you can create a `.campfire_export.yaml` file in your home
44
+ directory using this template:
45
+
46
+ # Your Campfire subdomain (for 'https://myco.campfirenow.com', use 'myco').
47
+ subdomain: example
48
+
49
+ # Your Campfire API token (see "My Info" on your Campfire site).
50
+ api_token: abababababababababababababababababababab
51
+
52
+ # OPTIONAL: Export start date - the first transcript you want exported.
53
+ # Uncomment to set. Defaults to the date each room was created.
54
+ #start_date: 2010/1/1
55
+
56
+ # OPTIONAL: Export end date - the last transcript you want exported.
57
+ # Uncomment to set. Defaults to the date of the last comment in each room.
58
+ #end_date: 2010/12/31
59
+
60
+ The `start_date` and `end_date` variables are inclusive (that is, if your
61
+ end date is Dec 31, 2010, a transcript for that date will be downloaded), and
62
+ both are optional. If they are omitted, export will run from the date each
63
+ Campfire room was created, until the date of the last message in that room.
64
+
65
+ ## Exporting ##
66
+
67
+ Just run `campfire_export` and your transcripts will be exported into a
68
+ `campfire` directory in the current directory, with subdirectories for each
69
+ site/room/year/month/day. In those directories, any uploaded files will be
70
+ saved with their original filenames, in a directory named for the upload ID
71
+ (since transcripts often have the same filename uploaded multiple times, e.g.
72
+ `Picture 1.png`). (Note that rooms and uploaded files may have odd filenames
73
+ -- for instance, spaces in the file/directory names.) Errors that happen
74
+ trying to export will be logged to `campfire/export_errors.txt`.
75
+
76
+ The Gist I forked had a plaintext transcript export, which I've kept in as
77
+ `transcript.txt` in each directory. However, the original XML and HTML are now
78
+ also saved as `transcript.xml` and `transcript.html`, which could be useful.
79
+
80
+ Days which have no messages posted will be ignored, so the resulting directory
81
+ structure will be sparse (no messages == no directory).
82
+
83
+ ## Credit ##
84
+
85
+ As mentioned above, some of the work on this was done by other people. The
86
+ Gist I forked had contributions from:
87
+
88
+ * [Pat Allan](https://github.com/freelancing-god)
89
+ * [Bruno Mattarollo](https://github.com/bruno)
90
+ * [bf4](https://github.com/bf4)
91
+
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
+ Thanks, all!
98
+
99
+ - Marc Hedlund, marc@precipice.org
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Copyright 2011 Marc Hedlund <marc@precipice.org>
4
+
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ # campfire_export -- export Campfire transcripts and uploaded files.
18
+ #
19
+ # Since Campfire (www.campfirenow.com) doesn't provide an export feature,
20
+ # this script implements one via the Campfire API.
21
+ #
22
+ # See https://github.com/precipice/campfire_export/blob/master/README.md
23
+ # for more information on using this script.
24
+
25
+ lib = File.expand_path(File.join(File.dirname(__FILE__), '../lib'))
26
+ $LOAD_PATH.unshift(lib) if File.directory?(lib) && !$LOAD_PATH.include?(lib)
27
+
28
+ require 'campfire_export'
29
+
30
+ def config_date(config, date_key)
31
+ date_str = config.fetch(date_key, nil)
32
+ date_str.nil? ? nil : Date.parse(date_str)
33
+ end
34
+
35
+ def ensure_config_for(config, key, prompt)
36
+ value = config.fetch(key, "")
37
+ while value == ""
38
+ print "#{prompt}: "
39
+ value = gets.chomp
40
+ end
41
+ config[key] = value
42
+ end
43
+
44
+ config = {}
45
+ config_file = File.join(ENV['HOME'], '.campfire_export.yaml')
46
+
47
+ if File.exists?(config_file) and File.readable?(config_file)
48
+ config = YAML.load_file(config_file)
49
+ end
50
+
51
+ ensure_config_for(config, 'subdomain',
52
+ "Your Campfire subdomain (for 'https://myco.campfirenow.com', use 'myco')")
53
+ ensure_config_for(config, 'api_token',
54
+ "Your Campfire API token (see 'My Info' on your Campfire site)")
55
+
56
+ account = CampfireExport::Account.new(config['subdomain'], config['api_token'])
57
+ account.export(config_date(config, 'start_date'), config_date(config, 'end_date'))
@@ -0,0 +1,26 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "campfire_export/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "campfire_export"
7
+ s.version = CampfireExport::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Marc Hedlund"]
10
+ s.email = ["marc@precipice.org"]
11
+ s.homepage = "https://github.com/precipice/campfire_export"
12
+ s.summary = %q{Export transcripts and uploaded files from your 37signals' Campfire account.}
13
+ s.description = s.summary
14
+
15
+ s.rubyforge_project = "campfire_export"
16
+
17
+ s.add_development_dependency "bundler", "~> 1.0.15"
18
+ s.add_development_dependency "tzinfo", "~> 0.3.29"
19
+ s.add_development_dependency "httparty", "~> 0.7.8"
20
+ s.add_development_dependency "nokogiri", "~> 1.4.5"
21
+
22
+ s.files = `git ls-files`.split("\n")
23
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
24
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
25
+ s.require_paths = ["lib"]
26
+ end
@@ -0,0 +1,168 @@
1
+ # Ruby on Rails is released under the MIT license.
2
+
3
+ require 'rubygems'
4
+
5
+ require 'tzinfo'
6
+
7
+ module CampfireExport
8
+ # This is a total cut & paste from:
9
+ # https://github.com/rails/rails/blob/master/activesupport/lib/active_support/values/time_zone.rb
10
+ # I'm copying it here to avoid bugs in the current active_support gem, to
11
+ # avoid having a dependency on active_support that might freak out Rails
12
+ # users, and to avoid fighting with RubyGems about threads and deprecation.
13
+ # See for background:
14
+ # https://github.com/rails/rails/pull/1215
15
+ # http://stackoverflow.com/questions/5176782/uninitialized-constant-activesupportdependenciesmutex-nameerror
16
+ module TimeZone
17
+ # Keys are Rails TimeZone names, values are TZInfo identifiers
18
+ MAPPING = {
19
+ "International Date Line West" => "Pacific/Midway",
20
+ "Midway Island" => "Pacific/Midway",
21
+ "Samoa" => "Pacific/Pago_Pago",
22
+ "Hawaii" => "Pacific/Honolulu",
23
+ "Alaska" => "America/Juneau",
24
+ "Pacific Time (US & Canada)" => "America/Los_Angeles",
25
+ "Tijuana" => "America/Tijuana",
26
+ "Mountain Time (US & Canada)" => "America/Denver",
27
+ "Arizona" => "America/Phoenix",
28
+ "Chihuahua" => "America/Chihuahua",
29
+ "Mazatlan" => "America/Mazatlan",
30
+ "Central Time (US & Canada)" => "America/Chicago",
31
+ "Saskatchewan" => "America/Regina",
32
+ "Guadalajara" => "America/Mexico_City",
33
+ "Mexico City" => "America/Mexico_City",
34
+ "Monterrey" => "America/Monterrey",
35
+ "Central America" => "America/Guatemala",
36
+ "Eastern Time (US & Canada)" => "America/New_York",
37
+ "Indiana (East)" => "America/Indiana/Indianapolis",
38
+ "Bogota" => "America/Bogota",
39
+ "Lima" => "America/Lima",
40
+ "Quito" => "America/Lima",
41
+ "Atlantic Time (Canada)" => "America/Halifax",
42
+ "Caracas" => "America/Caracas",
43
+ "La Paz" => "America/La_Paz",
44
+ "Santiago" => "America/Santiago",
45
+ "Newfoundland" => "America/St_Johns",
46
+ "Brasilia" => "America/Sao_Paulo",
47
+ "Buenos Aires" => "America/Argentina/Buenos_Aires",
48
+ "Georgetown" => "America/Guyana",
49
+ "Greenland" => "America/Godthab",
50
+ "Mid-Atlantic" => "Atlantic/South_Georgia",
51
+ "Azores" => "Atlantic/Azores",
52
+ "Cape Verde Is." => "Atlantic/Cape_Verde",
53
+ "Dublin" => "Europe/Dublin",
54
+ "Edinburgh" => "Europe/London",
55
+ "Lisbon" => "Europe/Lisbon",
56
+ "London" => "Europe/London",
57
+ "Casablanca" => "Africa/Casablanca",
58
+ "Monrovia" => "Africa/Monrovia",
59
+ "UTC" => "Etc/UTC",
60
+ "Belgrade" => "Europe/Belgrade",
61
+ "Bratislava" => "Europe/Bratislava",
62
+ "Budapest" => "Europe/Budapest",
63
+ "Ljubljana" => "Europe/Ljubljana",
64
+ "Prague" => "Europe/Prague",
65
+ "Sarajevo" => "Europe/Sarajevo",
66
+ "Skopje" => "Europe/Skopje",
67
+ "Warsaw" => "Europe/Warsaw",
68
+ "Zagreb" => "Europe/Zagreb",
69
+ "Brussels" => "Europe/Brussels",
70
+ "Copenhagen" => "Europe/Copenhagen",
71
+ "Madrid" => "Europe/Madrid",
72
+ "Paris" => "Europe/Paris",
73
+ "Amsterdam" => "Europe/Amsterdam",
74
+ "Berlin" => "Europe/Berlin",
75
+ "Bern" => "Europe/Berlin",
76
+ "Rome" => "Europe/Rome",
77
+ "Stockholm" => "Europe/Stockholm",
78
+ "Vienna" => "Europe/Vienna",
79
+ "West Central Africa" => "Africa/Algiers",
80
+ "Bucharest" => "Europe/Bucharest",
81
+ "Cairo" => "Africa/Cairo",
82
+ "Helsinki" => "Europe/Helsinki",
83
+ "Kyiv" => "Europe/Kiev",
84
+ "Riga" => "Europe/Riga",
85
+ "Sofia" => "Europe/Sofia",
86
+ "Tallinn" => "Europe/Tallinn",
87
+ "Vilnius" => "Europe/Vilnius",
88
+ "Athens" => "Europe/Athens",
89
+ "Istanbul" => "Europe/Istanbul",
90
+ "Minsk" => "Europe/Minsk",
91
+ "Jerusalem" => "Asia/Jerusalem",
92
+ "Harare" => "Africa/Harare",
93
+ "Pretoria" => "Africa/Johannesburg",
94
+ "Moscow" => "Europe/Moscow",
95
+ "St. Petersburg" => "Europe/Moscow",
96
+ "Volgograd" => "Europe/Moscow",
97
+ "Kuwait" => "Asia/Kuwait",
98
+ "Riyadh" => "Asia/Riyadh",
99
+ "Nairobi" => "Africa/Nairobi",
100
+ "Baghdad" => "Asia/Baghdad",
101
+ "Tehran" => "Asia/Tehran",
102
+ "Abu Dhabi" => "Asia/Muscat",
103
+ "Muscat" => "Asia/Muscat",
104
+ "Baku" => "Asia/Baku",
105
+ "Tbilisi" => "Asia/Tbilisi",
106
+ "Yerevan" => "Asia/Yerevan",
107
+ "Kabul" => "Asia/Kabul",
108
+ "Ekaterinburg" => "Asia/Yekaterinburg",
109
+ "Islamabad" => "Asia/Karachi",
110
+ "Karachi" => "Asia/Karachi",
111
+ "Tashkent" => "Asia/Tashkent",
112
+ "Chennai" => "Asia/Kolkata",
113
+ "Kolkata" => "Asia/Kolkata",
114
+ "Mumbai" => "Asia/Kolkata",
115
+ "New Delhi" => "Asia/Kolkata",
116
+ "Kathmandu" => "Asia/Kathmandu",
117
+ "Astana" => "Asia/Dhaka",
118
+ "Dhaka" => "Asia/Dhaka",
119
+ "Sri Jayawardenepura" => "Asia/Colombo",
120
+ "Almaty" => "Asia/Almaty",
121
+ "Novosibirsk" => "Asia/Novosibirsk",
122
+ "Rangoon" => "Asia/Rangoon",
123
+ "Bangkok" => "Asia/Bangkok",
124
+ "Hanoi" => "Asia/Bangkok",
125
+ "Jakarta" => "Asia/Jakarta",
126
+ "Krasnoyarsk" => "Asia/Krasnoyarsk",
127
+ "Beijing" => "Asia/Shanghai",
128
+ "Chongqing" => "Asia/Chongqing",
129
+ "Hong Kong" => "Asia/Hong_Kong",
130
+ "Urumqi" => "Asia/Urumqi",
131
+ "Kuala Lumpur" => "Asia/Kuala_Lumpur",
132
+ "Singapore" => "Asia/Singapore",
133
+ "Taipei" => "Asia/Taipei",
134
+ "Perth" => "Australia/Perth",
135
+ "Irkutsk" => "Asia/Irkutsk",
136
+ "Ulaan Bataar" => "Asia/Ulaanbaatar",
137
+ "Seoul" => "Asia/Seoul",
138
+ "Osaka" => "Asia/Tokyo",
139
+ "Sapporo" => "Asia/Tokyo",
140
+ "Tokyo" => "Asia/Tokyo",
141
+ "Yakutsk" => "Asia/Yakutsk",
142
+ "Darwin" => "Australia/Darwin",
143
+ "Adelaide" => "Australia/Adelaide",
144
+ "Canberra" => "Australia/Melbourne",
145
+ "Melbourne" => "Australia/Melbourne",
146
+ "Sydney" => "Australia/Sydney",
147
+ "Brisbane" => "Australia/Brisbane",
148
+ "Hobart" => "Australia/Hobart",
149
+ "Vladivostok" => "Asia/Vladivostok",
150
+ "Guam" => "Pacific/Guam",
151
+ "Port Moresby" => "Pacific/Port_Moresby",
152
+ "Magadan" => "Asia/Magadan",
153
+ "Solomon Is." => "Asia/Magadan",
154
+ "New Caledonia" => "Pacific/Noumea",
155
+ "Fiji" => "Pacific/Fiji",
156
+ "Kamchatka" => "Asia/Kamchatka",
157
+ "Marshall Is." => "Pacific/Majuro",
158
+ "Auckland" => "Pacific/Auckland",
159
+ "Wellington" => "Pacific/Auckland",
160
+ "Nuku'alofa" => "Pacific/Tongatapu"
161
+ }.each { |name, zone| name.freeze; zone.freeze }
162
+ MAPPING.freeze
163
+
164
+ def find_tzinfo(name)
165
+ TZInfo::TimezoneProxy.new(MAPPING[name] || name)
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,3 @@
1
+ module CampfireExport
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,435 @@
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 'campfire_export/timezone'
26
+
27
+ require 'cgi'
28
+ require 'fileutils'
29
+ require 'httparty'
30
+ require 'nokogiri'
31
+ require 'time'
32
+ require 'yaml'
33
+
34
+ module CampfireExport
35
+ module IO
36
+ def api_url(path)
37
+ "#{CampfireExport::Account.base_url}#{path}"
38
+ end
39
+
40
+ def get(path, params = {})
41
+ url = api_url(path)
42
+ response = HTTParty.get(url, :query => params, :basic_auth =>
43
+ {:username => CampfireExport::Account.api_token, :password => 'X'})
44
+
45
+ if response.code >= 400
46
+ raise CampfireExport::Exception.new(url, response.message, response.code)
47
+ end
48
+ response
49
+ end
50
+
51
+ def zero_pad(number)
52
+ "%02d" % number
53
+ end
54
+
55
+ # Requires that room and date be defined in the calling object.
56
+ def export_dir
57
+ "campfire/#{CampfireExport::Account.subdomain}/#{room.name}/" +
58
+ "#{date.year}/#{zero_pad(date.mon)}/#{zero_pad(date.day)}"
59
+ end
60
+
61
+ # Requires that room_name and date be defined in the calling object.
62
+ def export_file(content, filename, mode='w')
63
+ # Check to make sure we're writing into the target directory tree.
64
+ true_path = File.expand_path(File.join(export_dir, filename))
65
+ unless true_path.start_with?(File.expand_path(export_dir))
66
+ 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}).")
69
+ end
70
+
71
+ if File.exists?("#{export_dir}/#{filename}")
72
+ log(:error, "#{export_dir}/#{filename} failed: file already exists.")
73
+ else
74
+ 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
81
+ end
82
+ end
83
+ end
84
+
85
+ def verify_export(filename, expected_size)
86
+ full_path = "#{export_dir}/#{filename}"
87
+ unless File.exists?(full_path)
88
+ raise CampfireExport::Exception.new(full_path,
89
+ "file should have been exported but does not exist")
90
+ end
91
+ unless File.size(full_path) == expected_size
92
+ raise CampfireExport::Exception.new(full_path,
93
+ "exported file exists but is not the right size " +
94
+ "(expected: #{expected_size}, actual: #{File.size(full_path)})")
95
+ end
96
+ end
97
+
98
+ def log(level, message)
99
+ case level
100
+ when :error
101
+ puts "*** Error: #{message}"
102
+ open("campfire/export_errors.txt", 'a') do |log|
103
+ log.write "#{message}\n"
104
+ end
105
+ else
106
+ print message
107
+ STDOUT.flush
108
+ end
109
+ end
110
+ end
111
+
112
+ class Exception < StandardError
113
+ attr_accessor :resource, :message, :code
114
+ def initialize(resource, message, code=nil)
115
+ @resource = resource
116
+ @message = message
117
+ @code = code
118
+ end
119
+
120
+ def to_s
121
+ "<#{resource}>: #{message}" + (code ? " (#{code})" : "")
122
+ end
123
+ end
124
+
125
+ class Account
126
+ include CampfireExport::IO
127
+ include CampfireExport::TimeZone
128
+
129
+ @subdomain = ""
130
+ @api_token = ""
131
+ @base_url = ""
132
+ @timezone = ""
133
+
134
+ class << self
135
+ attr_accessor :subdomain, :api_token, :base_url, :timezone
136
+ end
137
+
138
+ def initialize(subdomain, api_token)
139
+ CampfireExport::Account.subdomain = subdomain
140
+ CampfireExport::Account.api_token = api_token
141
+ CampfireExport::Account.base_url = "https://#{subdomain}.campfirenow.com"
142
+ CampfireExport::Account.timezone = parse_timezone
143
+ end
144
+
145
+ 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)
150
+ end
151
+
152
+ def export(start_date=nil, end_date=nil)
153
+ begin
154
+ doc = Nokogiri::XML get('/rooms.xml').body
155
+ doc.css('room').each do |room_xml|
156
+ room = CampfireExport::Room.new(room_xml)
157
+ room.export(start_date, end_date)
158
+ end
159
+ rescue CampfireExport::Exception => e
160
+ log(:error, "room list download failed: #{e}")
161
+ end
162
+ end
163
+ end
164
+
165
+ class Room
166
+ include CampfireExport::IO
167
+ attr_accessor :id, :name, :created_at, :last_update
168
+
169
+ def initialize(room_xml)
170
+ @id = room_xml.css('id').text
171
+ @name = room_xml.css('name').text
172
+
173
+ created_utc = DateTime.parse(room_xml.css('created-at').text)
174
+ @created_at = CampfireExport::Account.timezone.utc_to_local(created_utc)
175
+
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
+
180
+ end
181
+
182
+ def export(start_date=nil, end_date=nil)
183
+ # Figure out how to do the least amount of work while still conforming
184
+ # to the requester's boundary dates.
185
+ start_date.nil? ? date = created_at : date = [start_date, created_at].max
186
+ end_date.nil? ? end_date = last_update : end_date = [end_date, last_update].min
187
+
188
+ while date <= end_date
189
+ transcript = CampfireExport::Transcript.new(self, date)
190
+ transcript.export
191
+
192
+ # Ensure that we stay well below the 37signals API limits.
193
+ sleep(1.0/10.0)
194
+ date = date.next
195
+ end
196
+ end
197
+ end
198
+
199
+ class Transcript
200
+ include CampfireExport::IO
201
+ attr_accessor :room, :date, :messages
202
+
203
+ def initialize(room, date)
204
+ @room = room
205
+ @date = date
206
+ end
207
+
208
+ def transcript_path
209
+ "/room/#{room.id}/transcript/#{date.year}/#{date.mon}/#{date.mday}"
210
+ end
211
+
212
+ def export
213
+ begin
214
+ log(:info, "#{export_dir} ... ")
215
+ transcript_xml = Nokogiri::XML get("#{transcript_path}.xml").body
216
+
217
+ @messages = transcript_xml.css('message').map do |message|
218
+ CampfireExport::Message.new(message, room, date)
219
+ end
220
+
221
+ # Only export transcripts that contain at least one message.
222
+ if messages.length > 0
223
+ 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
232
+ else
233
+ log(:info, "no messages\n")
234
+ end
235
+ rescue CampfireExport::Exception => e
236
+ log(:error, "transcript export for #{export_dir} failed: #{e}")
237
+ end
238
+ end
239
+
240
+ def export_plaintext
241
+ begin
242
+ plaintext = "#{CampfireExport::Account.subdomain.upcase} CAMPFIRE\n"
243
+ plaintext << "#{room.name}: " +
244
+ "#{date.strftime('%A, %B %e, %Y').gsub(' ', ' ')}\n\n"
245
+ messages.each {|message| plaintext << message.to_s }
246
+ export_file(plaintext, 'transcript.txt')
247
+ verify_export('transcript.txt', plaintext.length)
248
+ rescue CampfireExport::Exception => e
249
+ log(:error, "Plaintext transcript export for #{export_dir} failed: #{e}")
250
+ end
251
+ end
252
+
253
+ def export_html
254
+ begin
255
+ transcript_html = get(transcript_path)
256
+ # Make the upload links in the transcript clickable for the exported
257
+ # directory layout.
258
+ transcript_html.gsub!(%Q{<a href="/room/#{room.id}/uploads/},
259
+ %Q{<a href="uploads/})
260
+ export_file(transcript_html, 'transcript.html')
261
+ verify_export('transcript.html', transcript_html.length)
262
+ rescue CampfireExport::Exception => e
263
+ log(:error, "HTML transcript export for #{export_dir} failed: #{e}")
264
+ end
265
+ end
266
+
267
+ def export_uploads
268
+ messages.each do |message|
269
+ if message.is_upload?
270
+ begin
271
+ message.upload.export
272
+ rescue CampfireExport::Exception => e
273
+ path = "#{message.upload.export_dir}/#{message.upload.filename}"
274
+ log(:error, "Upload export for #{path} failed: " +
275
+ "#{e.backtrace.join("\n")}")
276
+ end
277
+ end
278
+ end
279
+ end
280
+ end
281
+
282
+ class Message
283
+ include CampfireExport::IO
284
+ attr_accessor :id, :room, :body, :type, :user, :date, :timestamp, :upload
285
+
286
+ def initialize(message, room, date)
287
+ @id = message.css('id').text
288
+ @room = room
289
+ @date = date
290
+ @body = message.css('body').text
291
+ @type = message.css('type').text
292
+
293
+ time = Time.parse message.css('created-at').text
294
+ localtime = CampfireExport::Account.timezone.utc_to_local(time)
295
+ @timestamp = localtime.strftime '%I:%M %p'
296
+
297
+ no_user = ['TimestampMessage', 'SystemMessage', 'AdvertisementMessage']
298
+ 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
304
+ end
305
+
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
311
+ end
312
+
313
+ def username(user_id)
314
+ @@usernames ||= {}
315
+ @@usernames[user_id] ||= begin
316
+ doc = Nokogiri::XML get("/users/#{user_id}.xml").body
317
+ doc.css('name').text
318
+ end
319
+ end
320
+
321
+ def is_upload?
322
+ @type == 'UploadMessage'
323
+ end
324
+
325
+ def indent(string, count)
326
+ (' ' * count) + string.gsub(/(\n+)/) { $1 + (' ' * count) }
327
+ end
328
+
329
+ def to_s
330
+ case type
331
+ when 'EnterMessage'
332
+ "[#{user} has entered the room]\n"
333
+ when 'KickMessage', 'LeaveMessage'
334
+ "[#{user} has left the room]\n"
335
+ when 'TextMessage'
336
+ "[#{user}:] #{body}\n"
337
+ when 'UploadMessage'
338
+ "[#{user} uploaded:] #{body}\n"
339
+ when 'PasteMessage'
340
+ "[#{user} pasted:]\n#{indent(body, 2)}\n"
341
+ when 'TopicChangeMessage'
342
+ "[#{user} changed the topic to: #{body}]\n"
343
+ when 'ConferenceCreatedMessage'
344
+ "[#{user} created conference: #{body}]\n"
345
+ when 'AllowGuestsMessage'
346
+ "[#{user} opened the room to guests]\n"
347
+ when 'DisallowGuestsMessage'
348
+ "[#{user} closed the room to guests]\n"
349
+ when 'LockMessage'
350
+ "[#{user} locked the room]\n"
351
+ when 'UnlockMessage'
352
+ "[#{user} unlocked the room]\n"
353
+ when 'IdleMessage'
354
+ "[#{user} became idle]\n"
355
+ when 'UnidleMessage'
356
+ "[#{user} became active]\n"
357
+ when 'TweetMessage'
358
+ "[#{user} tweeted:] #{body}\n"
359
+ when 'SoundMessage'
360
+ "[#{user} played a sound:] #{body}\n"
361
+ when 'TimestampMessage'
362
+ "--- #{timestamp} ---\n"
363
+ when 'SystemMessage'
364
+ ""
365
+ when 'AdvertisementMessage'
366
+ ""
367
+ else
368
+ log(:error, "unknown message type: #{type} - '#{body}'")
369
+ ""
370
+ end
371
+ end
372
+ end
373
+
374
+ class Upload
375
+ include CampfireExport::IO
376
+ attr_accessor :message, :room, :date, :id, :filename, :content, :byte_size
377
+
378
+ def initialize(message)
379
+ @message = message
380
+ @room = message.room
381
+ @date = message.date
382
+ @deleted = false
383
+ end
384
+
385
+ def deleted?
386
+ @deleted
387
+ end
388
+
389
+ def upload_dir
390
+ "uploads/#{id}"
391
+ end
392
+
393
+ def export
394
+ begin
395
+ log(:info, " #{message.body} ... ")
396
+
397
+ # Get the upload object corresponding to this message.
398
+ upload_path = "/room/#{room.id}/messages/#{message.id}/upload.xml"
399
+ upload = Nokogiri::XML get(upload_path).body
400
+
401
+ # Get the upload itself and export it.
402
+ @id = upload.css('id').text
403
+ @byte_size = upload.css('byte-size').text.to_i
404
+ @filename = upload.css('name').text
405
+ escaped_name = CGI.escape(filename)
406
+
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)
419
+ log(:info, "ok\n")
420
+ rescue CampfireExport::Exception => e
421
+ if e.code == 404
422
+ # If the upload 404s, that should mean it was subsequently deleted.
423
+ @deleted = true
424
+ log(:info, "deleted\n")
425
+ else
426
+ log(:error, "Got an upload error: #{e.backtrace.join("\n")}")
427
+ raise e
428
+ end
429
+ rescue => e
430
+ log(:error, "export of #{export_dir}/#{upload_dir}/#{filename} failed:\n" +
431
+ "#{e}:\n#{e.backtrace.join("\n")}")
432
+ end
433
+ end
434
+ end
435
+ end
metadata ADDED
@@ -0,0 +1,138 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: campfire_export
3
+ version: !ruby/object:Gem::Version
4
+ hash: 29
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 1
10
+ version: 0.0.1
11
+ platform: ruby
12
+ authors:
13
+ - Marc Hedlund
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-07-08 00:00:00 Z
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: bundler
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ~>
27
+ - !ruby/object:Gem::Version
28
+ hash: 9
29
+ segments:
30
+ - 1
31
+ - 0
32
+ - 15
33
+ version: 1.0.15
34
+ type: :development
35
+ version_requirements: *id001
36
+ - !ruby/object:Gem::Dependency
37
+ name: tzinfo
38
+ prerelease: false
39
+ requirement: &id002 !ruby/object:Gem::Requirement
40
+ none: false
41
+ requirements:
42
+ - - ~>
43
+ - !ruby/object:Gem::Version
44
+ hash: 41
45
+ segments:
46
+ - 0
47
+ - 3
48
+ - 29
49
+ version: 0.3.29
50
+ type: :development
51
+ version_requirements: *id002
52
+ - !ruby/object:Gem::Dependency
53
+ name: httparty
54
+ prerelease: false
55
+ requirement: &id003 !ruby/object:Gem::Requirement
56
+ none: false
57
+ requirements:
58
+ - - ~>
59
+ - !ruby/object:Gem::Version
60
+ hash: 19
61
+ segments:
62
+ - 0
63
+ - 7
64
+ - 8
65
+ version: 0.7.8
66
+ type: :development
67
+ version_requirements: *id003
68
+ - !ruby/object:Gem::Dependency
69
+ name: nokogiri
70
+ prerelease: false
71
+ requirement: &id004 !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ~>
75
+ - !ruby/object:Gem::Version
76
+ hash: 13
77
+ segments:
78
+ - 1
79
+ - 4
80
+ - 5
81
+ version: 1.4.5
82
+ type: :development
83
+ version_requirements: *id004
84
+ description: Export transcripts and uploaded files from your 37signals' Campfire account.
85
+ email:
86
+ - marc@precipice.org
87
+ executables:
88
+ - campfire_export
89
+ extensions: []
90
+
91
+ extra_rdoc_files: []
92
+
93
+ files:
94
+ - .gitignore
95
+ - Gemfile
96
+ - LICENSE.txt
97
+ - README.md
98
+ - Rakefile
99
+ - bin/campfire_export
100
+ - campfire_export.gemspec
101
+ - lib/campfire_export.rb
102
+ - lib/campfire_export/timezone.rb
103
+ - lib/campfire_export/version.rb
104
+ homepage: https://github.com/precipice/campfire_export
105
+ licenses: []
106
+
107
+ post_install_message:
108
+ rdoc_options: []
109
+
110
+ require_paths:
111
+ - lib
112
+ required_ruby_version: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ hash: 3
118
+ segments:
119
+ - 0
120
+ version: "0"
121
+ required_rubygems_version: !ruby/object:Gem::Requirement
122
+ none: false
123
+ requirements:
124
+ - - ">="
125
+ - !ruby/object:Gem::Version
126
+ hash: 3
127
+ segments:
128
+ - 0
129
+ version: "0"
130
+ requirements: []
131
+
132
+ rubyforge_project: campfire_export
133
+ rubygems_version: 1.8.5
134
+ signing_key:
135
+ specification_version: 3
136
+ summary: Export transcripts and uploaded files from your 37signals' Campfire account.
137
+ test_files: []
138
+