stackbuilders-campfire_export 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,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