campfire_export 0.0.1

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/.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
+