Convert the messages.json file from an exported Flowdock flow into a static HTML page.
#!/usr/bin/env ruby
# Convert an exported Flowdock flow into a static HTML document.
#
# Usage:
#
# flowdock-archive-to-html messages.json > messages.html
#
# The script assumes that there is a subdirectory `files` containing all files referenced in the exported flow. (This is exactly the
# directory structure you get if you unzip an archive downloaded from Flowdock.)
#
# Only flow events of the types “message”, “comment”, and “file” are handled and thus included in the output. To include
# other events like “user-edit” or “mail”, add the appropriate formatting code to the huge `case` statement at the end of
# this file.
require "bundler/inline"
require "erb"
require "json"
gemfile do
source "https://rubygems.org"
gem "redcarpet", "~> 2.3.0"
end
# Add “userid => username” mappings for all users you want to identify by name. Missing users will show up as “User 123456”.
@usernames = {
"0" => "Flowdock",
}
HTML_HEADER = <<~HTML.freeze
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style type="text/css">
html {
font-family: sans-serif;
font-size: 14px;
line-height: 1.5;
}
.message {
display: flex;
flex-direction: row;
margin: 0 0 1em;
padding: 0 0 1em;
border-bottom: 1px solid #f4f4f4;
}
.message p {
margin: 0;
overflow-wrap: break-word;
}
.message pre {
background: #eee;
padding: 0.5em 1em;
max-width: 100%;
overflow: scroll;
}
.message img {
max-width: 75%;
max-height: 50vh;
}
.message blockquote {
background: #f6f6f9;
padding: 0.5em 1em;
border-left: 3px solid #9898B0;
margin: 0 0 1em;
color: #2E2E75;
}
.date {
flex: 0 14em;
color: #999;
font-size: 80%;
text-align: right;
}
.user {
flex: 0 5em;
color: #999;
font-weight: bold;
text-align: right;
margin-right: 1em;
}
.content {
min-width: 0; /* To make `max-width: 100%` work in contained elements, see http://stackoverflow.com/a/31972181/566850 */
flex: 1;
}
</style>
</head>
<body>
HTML
HTML_FOOTER = <<~HTML.freeze
</body>
</html>
HTML
@markdown = Redcarpet::Markdown.new(Redcarpet::Render::HTML.new(escape_html: true, hard_wrap: true), autolink: true, no_intra_emphasis: true, space_after_headers: true)
def h(str)
ERB::Util.h(str)
end
def markdown(str)
@markdown.render(str.to_s)
end
def render_message(message, content)
date = Time.at(message['sent'] / 1000)
user = @usernames.fetch(message['user']) do
fallback_username = "User #{message['user']}"
$stderr.puts "WARNING: No username mapping for userid #{message['user']} - using '#{fallback_username}' instead."
@usernames[message['user']] = fallback_username
end
puts <<~HTML
<div class='message'>
<div class='user'>#{h user}</div>
<div class='content'>#{content}</div>
<div class='date'>#{date.strftime('%H:%M – %b. %d, %Y')}</div>
</div>
HTML
end
puts HTML_HEADER
JSON.parse(File.read("messages.json")).each do |message|
event = message["event"]
case event
when "message", "comment"
content =
if event == "comment"
message['content']['title'].gsub(/^/, '> \1') + "\n\n" + message['content']['text']
else
message['content']
end
render_message(message, markdown(content))
when "file"
path = "files/" + message["content"]["path"].gsub(%r{\A/files/\d+/}, "").tr("/", "_")
link_text =
if message["content"].key?("image")
"<img src='#{path}'>"
else
h(message["content"]["file_name"])
end
render_message(message, "<a href='#{path}'>#{link_text}</a>")
when "action", "user-edit", "mail", "open-invitation-enable", "line", "status", "discussion", "activity"
# ignore
else
$stderr.puts "WARNING: Unknown event type in JSON data: `#{message['event']}`"
$stderr.puts message.inspect + "\n\n"
end
end
puts HTML_FOOTER