stackbuilders-campfire_export 0.4.0

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.
@@ -0,0 +1,32 @@
1
+ module CampfireExport
2
+ class Account
3
+ include CampfireExport::IO
4
+ include CampfireExport::TimeZone
5
+
6
+ @subdomain = ""
7
+ @api_token = ""
8
+ @base_url = ""
9
+ @timezone = nil
10
+
11
+ class << self
12
+ attr_accessor :subdomain, :api_token, :base_url, :timezone
13
+ end
14
+
15
+ def initialize(subdomain, api_token)
16
+ Account.subdomain = subdomain
17
+ Account.api_token = api_token
18
+ Account.base_url = "https://#{subdomain}.campfirenow.com"
19
+ end
20
+
21
+ def find_timezone
22
+ settings = Nokogiri::XML get('/account.xml').body
23
+ selected_zone = settings.xpath('/account/time-zone')
24
+ Account.timezone = find_tzinfo(selected_zone.text)
25
+ end
26
+
27
+ def rooms
28
+ doc = Nokogiri::XML get('/rooms.xml').body
29
+ doc.xpath('/rooms/room').map {|room_xml| Room.new(room_xml) }
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,16 @@
1
+ module CampfireExport
2
+ class Exception < StandardError
3
+
4
+ attr_reader :resource, :message, :code
5
+
6
+ def initialize(resource, message, code=nil)
7
+ @resource = resource
8
+ @message = message
9
+ @code = code
10
+ end
11
+
12
+ def to_s
13
+ "<#{resource}>: #{message}" + (code ? " (#{code})" : "")
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,95 @@
1
+ module CampfireExport
2
+ class Message
3
+ include CampfireExport::IO
4
+ attr_accessor :id, :room, :body, :type, :user, :date, :timestamp, :upload
5
+
6
+ def initialize(message, room, date)
7
+ @id = message.xpath('id').text
8
+ @room = room
9
+ @date = date
10
+ @body = message.xpath('body').text
11
+ @type = message.xpath('type').text
12
+
13
+ time = Time.parse message.xpath('created-at').text
14
+ localtime = CampfireExport::Account.timezone.utc_to_local(time)
15
+ @timestamp = localtime.strftime '%I:%M %p'
16
+
17
+ no_user = ['TimestampMessage', 'SystemMessage', 'AdvertisementMessage']
18
+ unless no_user.include?(@type)
19
+ @user = username(message.xpath('user-id').text)
20
+ end
21
+
22
+ @upload = CampfireExport::Upload.new(self) if is_upload?
23
+ end
24
+
25
+ def username(user_id)
26
+ @@usernames ||= {}
27
+ @@usernames[user_id] ||= begin
28
+ doc = Nokogiri::XML get("/users/#{user_id}.xml").body
29
+ rescue => e
30
+ "[unknown user]"
31
+ else
32
+ # Take the first name and last initial, if there is more than one name.
33
+ name_parts = doc.xpath('/user/name').text.split
34
+ if name_parts.length > 1
35
+ name_parts[-1] = "#{name_parts.last[0,1]}."
36
+ name_parts.join(" ")
37
+ else
38
+ name_parts[0]
39
+ end
40
+ end
41
+ end
42
+
43
+ def is_upload?
44
+ @type == 'UploadMessage'
45
+ end
46
+
47
+ def indent(string, count)
48
+ (' ' * count) + string.gsub(/(\n+)/) { $1 + (' ' * count) }
49
+ end
50
+
51
+ def to_s
52
+ case type
53
+ when 'EnterMessage'
54
+ "[#{user} has entered the room]\n"
55
+ when 'KickMessage', 'LeaveMessage'
56
+ "[#{user} has left the room]\n"
57
+ when 'TextMessage'
58
+ "[#{user.rjust(12)}:] #{body}\n"
59
+ when 'UploadMessage'
60
+ "[#{user} uploaded: #{body}]\n"
61
+ when 'PasteMessage'
62
+ "[" + "#{user} pasted:]".rjust(14) + "\n#{indent(body, 16)}\n"
63
+ when 'TopicChangeMessage'
64
+ "[#{user} changed the topic to: #{body}]\n"
65
+ when 'ConferenceCreatedMessage'
66
+ "[#{user} created conference: #{body}]\n"
67
+ when 'AllowGuestsMessage'
68
+ "[#{user} opened the room to guests]\n"
69
+ when 'DisallowGuestsMessage'
70
+ "[#{user} closed the room to guests]\n"
71
+ when 'LockMessage'
72
+ "[#{user} locked the room]\n"
73
+ when 'UnlockMessage'
74
+ "[#{user} unlocked the room]\n"
75
+ when 'IdleMessage'
76
+ "[#{user} became idle]\n"
77
+ when 'UnidleMessage'
78
+ "[#{user} became active]\n"
79
+ when 'TweetMessage'
80
+ "[#{user} tweeted:] #{body}\n"
81
+ when 'SoundMessage'
82
+ "[#{user} played a sound:] #{body}\n"
83
+ when 'TimestampMessage'
84
+ "--- #{timestamp} ---\n"
85
+ when 'SystemMessage'
86
+ ""
87
+ when 'AdvertisementMessage'
88
+ ""
89
+ else
90
+ log(:error, "unknown message type: #{type} - '#{body}'")
91
+ ""
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,44 @@
1
+ module CampfireExport
2
+ class Room
3
+ include CampfireExport::IO
4
+ attr_accessor :id, :name, :created_at, :last_update
5
+
6
+ def initialize(room_xml)
7
+ @id = room_xml.xpath('id').text
8
+ @name = room_xml.xpath('name').text
9
+ created_utc = DateTime.parse(room_xml.xpath('created-at').text)
10
+ @created_at = Account.timezone.utc_to_local(created_utc)
11
+ end
12
+
13
+ def export(start_date=nil, end_date=nil)
14
+ # Figure out how to do the least amount of work while still conforming
15
+ # to the requester's boundary dates.
16
+ find_last_update
17
+ start_date.nil? ? date = created_at : date = [start_date, created_at].max
18
+ end_date.nil? ? end_date = last_update : end_date = [end_date, last_update].min
19
+
20
+ while date <= end_date
21
+ transcript = Transcript.new(self, date)
22
+ transcript.export
23
+
24
+ # Ensure that we stay well below the 37signals API limits.
25
+ sleep(1.0/10.0)
26
+ date = date.next
27
+ end
28
+ end
29
+
30
+ private
31
+ def find_last_update
32
+ begin
33
+ last_message = Nokogiri::XML get("/room/#{id}/recent.xml?limit=1").body
34
+ update_utc = DateTime.parse(last_message.xpath('/messages/message[1]/created-at').text)
35
+ @last_update = Account.timezone.utc_to_local(update_utc)
36
+ rescue => e
37
+ log(:error,
38
+ "couldn't get last update in #{room} (defaulting to today)",
39
+ e)
40
+ @last_update = Time.now
41
+ end
42
+ end
43
+ end
44
+ 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::Timezone.get(MAPPING[name] || name)
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,100 @@
1
+ module CampfireExport
2
+ class Transcript
3
+ include CampfireExport::IO
4
+ attr_accessor :room, :date, :xml, :messages
5
+
6
+ def initialize(room, date)
7
+ @room = room
8
+ @date = date
9
+ end
10
+
11
+ def transcript_path
12
+ "/room/#{room.id}/transcript/#{date.year}/#{date.mon}/#{date.mday}"
13
+ end
14
+
15
+ def export
16
+ begin
17
+ log(:info, "#{export_dir} ... ")
18
+ @xml = Nokogiri::XML get("#{transcript_path}.xml").body
19
+ rescue => e
20
+ log(:error, "transcript export for #{export_dir} failed", e)
21
+ else
22
+ @messages = xml.xpath('/messages/message').map do |message|
23
+ CampfireExport::Message.new(message, room, date)
24
+ end
25
+
26
+ # Only export transcripts that contain at least one message.
27
+ if messages.length > 0
28
+ log(:info, "exporting transcripts\n")
29
+ begin
30
+ FileUtils.mkdir_p export_dir
31
+ rescue => e
32
+ log(:error, "Unable to create #{export_dir}", e)
33
+ else
34
+ export_xml
35
+ export_plaintext
36
+ export_html
37
+ export_uploads
38
+ end
39
+ else
40
+ log(:info, "no messages\n")
41
+ end
42
+ end
43
+ end
44
+
45
+ def export_xml
46
+ begin
47
+ export_file(xml, 'transcript.xml')
48
+ verify_export('transcript.xml', xml.to_s.bytesize)
49
+ rescue => e
50
+ log(:error, "XML transcript export for #{export_dir} failed", e)
51
+ end
52
+ end
53
+
54
+ def export_plaintext
55
+ begin
56
+ date_header = date.strftime('%A, %B %e, %Y').squeeze(" ")
57
+ plaintext = "#{CampfireExport::Account.subdomain.upcase} CAMPFIRE\n"
58
+ plaintext << "#{room.name}: #{date_header}\n\n"
59
+ messages.each {|message| plaintext << message.to_s }
60
+ export_file(plaintext, 'transcript.txt')
61
+ verify_export('transcript.txt', plaintext.bytesize)
62
+ rescue => e
63
+ log(:error, "Plaintext transcript export for #{export_dir} failed", e)
64
+ end
65
+ end
66
+
67
+ def export_html
68
+ begin
69
+ transcript_html = get(transcript_path).to_s
70
+
71
+ # Make the upload links in the transcript clickable from the exported
72
+ # directory layout.
73
+ transcript_html.gsub!(%Q{href="/room/#{room.id}/uploads/},
74
+ %Q{href="uploads/})
75
+ # Likewise, make the image thumbnails embeddable from the exported
76
+ # directory layout.
77
+ transcript_html.gsub!(%Q{src="/room/#{room.id}/thumb/},
78
+ %Q{src="thumbs/})
79
+
80
+ export_file(transcript_html, 'transcript.html')
81
+ verify_export('transcript.html', transcript_html.bytesize)
82
+ rescue => e
83
+ log(:error, "HTML transcript export for #{export_dir} failed", e)
84
+ end
85
+ end
86
+
87
+ def export_uploads
88
+ messages.each do |message|
89
+ if message.is_upload?
90
+ begin
91
+ message.upload.export
92
+ rescue => e
93
+ path = "#{message.upload.export_dir}/#{message.upload.filename}"
94
+ log(:error, "Upload export for #{path} failed", e)
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end