summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorpdp8 <pdp8@pdp8.info>2023-07-01 00:57:48 +0200
committerpdp8 <pdp8@pdp8.info>2023-07-01 00:57:48 +0200
commit5af8d78e195c7479769240b32703d5b76843db4d (patch)
treee9d145e1318e3c47b5b2b00ce276703a14edc958
parent8dd940a1c08b02ed1ea613284b627e1f46fb13c9 (diff)
initial refactoring of client.rb
-rw-r--r--activitypub.rb2
-rw-r--r--client.rb201
-rw-r--r--helpers.rb82
-rw-r--r--server.rb35
-rw-r--r--views/collection.erb (renamed from views/index.erb)13
-rw-r--r--views/item.erb54
-rw-r--r--views/object.erb57
7 files changed, 217 insertions, 227 deletions
diff --git a/activitypub.rb b/activitypub.rb
index ae3c270..eefae9b 100644
--- a/activitypub.rb
+++ b/activitypub.rb
@@ -28,5 +28,5 @@ set :default_content_type, 'application/activity+json'
set :port, 9292
require_relative 'helpers'
-require_relative 'server'
require_relative 'client'
+require_relative 'server'
diff --git a/client.rb b/client.rb
index abf565d..b62f139 100644
--- a/client.rb
+++ b/client.rb
@@ -1,28 +1,30 @@
+# frozen_string_literal: true
+
# client-server
-post "/" do
+post '/' do
protected!
- date = Time.now.strftime("%Y-%m-%dT%H:%M:%S")
- outbox_path = File.join("public/outbox", date + ".json")
- notes_path = File.join("public/notes", date + ".json")
+ date = Time.now.strftime('%Y-%m-%dT%H:%M:%S')
+ outbox_path = File.join('public/outbox', "#{date}.json")
+ notes_path = File.join('public/notes', "#{date}.json")
- recipients = ["https://www.w3.org/ns/activitystreams#Public", params[:to]]
- recipients += Dir[File.join("public/followers", "*.json")].collect { |f| JSON.parse(File.read(f))["actor"] }
+ recipients = ['https://www.w3.org/ns/activitystreams#Public', params[:to]]
+ recipients += Dir[File.join('public/followers', '*.json')].collect { |f| JSON.parse(File.read(f))['actor'] }
recipients.delete ACTOR
recipients.uniq!
-
+
content = []
attachment = []
tag = []
extensions = {
- :image => ["jpeg","png"],
- :audio => ["flac","wav","mp3","ogg"],
- :video => ["mp4","webm"]
+ image: %w[jpeg png],
+ audio: %w[flac wav mp3 ogg],
+ video: %w[mp4 webm]
}
params[:content].lines.each do |line|
line.chomp!
if line.match(/^http/)
- ext = File.extname(line).sub('.','')
- media_type = extensions.select{|k,v| v.include? ext}.keys[0].to_s + '/' + ext
+ ext = File.extname(line).sub('.', '')
+ media_type = extensions.select { |_k, v| v.include? ext }.keys[0].to_s + '/' + ext
attachment << {
'type' => 'Document',
'mediaType' => media_type,
@@ -31,7 +33,7 @@ post "/" do
else
tags = line.split(/\s+/).grep(/^#\w+$/)
tags.each do |name|
- href = File.join(SOCIAL_URL,'tags',name.sub('#',''))
+ href = File.join(SOCIAL_URL, 'tags', name.sub('#', ''))
tag << {
'type' => 'Hashtag',
'href' => href,
@@ -44,161 +46,129 @@ post "/" do
end
create = {
- "@context" => "https://www.w3.org/ns/activitystreams",
- "id" => File.join(SOCIAL_URL, outbox_path),
- "type" => "Create",
- "actor" => ACTOR,
- "object" => {
- "id" => File.join(SOCIAL_URL, notes_path),
- "type" => "Note",
- "attributedTo" => ACTOR,
- "inReplyTo" => params[:inReplyTo],
- "published" => date,
- "content" => "<p>\n"+content.join("\n<br>")+"\n</p>",
- "attachment" => attachment,
- "tag" => tag,
- "to" => recipients
+ '@context' => 'https://www.w3.org/ns/activitystreams',
+ 'id' => File.join(SOCIAL_URL, outbox_path),
+ 'type' => 'Create',
+ 'actor' => ACTOR,
+ 'object' => {
+ 'id' => File.join(SOCIAL_URL, notes_path),
+ 'type' => 'Note',
+ 'attributedTo' => ACTOR,
+ 'inReplyTo' => params[:inReplyTo],
+ 'published' => date,
+ 'content' => "<p>\n#{content.join("\n<br>")}\n</p>",
+ 'attachment' => attachment,
+ 'tag' => tag,
+ 'to' => recipients
},
- "published" => date,
- "to" => recipients
+ 'published' => date,
+ 'to' => recipients
}
- File.open(outbox_path, "w+") { |f| f.puts create.to_json }
- File.open(notes_path, "w+") { |f| f.puts create["object"].to_json }
+ File.open(outbox_path, 'w+') { |f| f.puts create.to_json }
+ File.open(notes_path, 'w+') { |f| f.puts create['object'].to_json }
tag.each do |t|
- dir = File.join('public','tags',t['name'].sub('#',''))
+ dir = File.join('public', 'tags', t['name'].sub('#', ''))
FileUtils.mkdir_p dir
- FileUtils.ln_s File.join('/srv/social/',notes_path), dir
+ FileUtils.ln_s File.join('/srv/social/', notes_path), dir
end
- #recipients.delete "https://www.w3.org/ns/activitystreams#Public"
- #recipients.each { |r| send_signed create, r }
- send_signed create #, r }
+ # recipients.delete "https://www.w3.org/ns/activitystreams#Public"
+ # recipients.each { |r| send_signed create, r }
+ # send_signed create # , r }
+ outbox create
redirect params['redirect']
end
-post "/archive" do
+post '/archive' do
protected!
- FileUtils.mv params['file'], "archive/"
+ FileUtils.mv params['file'], 'archive/'
redirect to(params['redirect'])
end
-post "/delete" do # delete not supported by html forms
+post '/delete' do # delete not supported by html forms
protected!
- params['file'] ? FileUtils.rm_f(params['file']) : FileUtils.rm_f(Dir["inbox/*.json"])
- params['redirect'] ? redirect(params['redirect']) : redirect('/')
+ FileUtils.rm_f(params['file'] || Dir['inbox/*.json'])
+ redirect(params['redirect'] || '/')
end
-post "/follow" do
+post '/follow' do
protected!
actor, mention = parse_follow params['follow']
- follow = { "@context" => "https://www.w3.org/ns/activitystreams",
- "id" => File.join(SOCIAL_URL, "following", mention+ ".json"),
- "type" => "Follow",
- "actor" => ACTOR,
- "object" => actor,
- 'to' => [ actor ] }
- send_signed follow#, actor
- redirect "/"
+ follow = { '@context' => 'https://www.w3.org/ns/activitystreams',
+ 'id' => File.join(SOCIAL_URL, 'following', "#{mention}.json"),
+ 'type' => 'Follow',
+ 'actor' => ACTOR,
+ 'object' => actor,
+ 'to' => [actor] }
+ send_signed follow # , actor
+ redirect '/'
end
-post "/unfollow" do
+post '/unfollow' do
protected!
actor, mention = parse_follow params['follow']
- following_path = File.join("public", "following", mention + ".json")
- if File.exists?(following_path)
- undo = { "@context" => "https://www.w3.org/ns/activitystreams",
- "id" => File.join(SOCIAL_URL + "#undo", SecureRandom.uuid),
- "type" => "Undo",
- "actor" => ACTOR,
- "object" => JSON.parse(File.read(following_path)),
- 'to' => [ actor ] }
- send_signed undo#, actor
+ following_path = File.join('public', 'following', "#{mention}.json")
+ if File.exist?(following_path)
+ undo = { '@context' => 'https://www.w3.org/ns/activitystreams',
+ 'id' => File.join("#{SOCIAL_URL}#undo", SecureRandom.uuid),
+ 'type' => 'Undo',
+ 'actor' => ACTOR,
+ 'object' => JSON.parse(File.read(following_path)),
+ 'to' => [actor] }
+ send_signed undo # , actor
FileUtils.rm following_path
- redirect "/"
+ redirect '/'
end
end
-post "/login" do
- session["client"] = true if OpenSSL::Digest::SHA256.base64digest(params["secret"]) == File.read(".digest").chomp
- redirect "/"
+post '/login' do
+ session['client'] = (OpenSSL::Digest::SHA256.base64digest(params['secret']) == File.read('.digest').chomp)
+ redirect '/'
+end
+
+get '/' do
+ redirect '/inbox'
end
-# private
-["/", "/archive"].each do |path|
- get path, :provides => 'html' do
+['/inbox', '/archive', '/outbox'].each do |path|
+ get path, provides: 'html' do
protected!
- if path == '/'
- @dir = 'inbox'
- @alt_dir = '/archive'
- @alt_name = 'archive'
- else
- @dir = path.sub('/','')
- @alt_dir = '/'
- @alt_name = 'inbox'
- end
+ @items = fetch(File.join(SOCIAL_URL, path))['orderedItems']
threads
- erb :index
+ erb :collection
end
end
helpers do
-
def protected!
halt 403 unless session['client']
end
- def items
- nr = 0
- p @dir
- p Dir[File.join(@dir, '*', '*.json')]# + Dir['public/notes/*.json']
- files = Dir[File.join(@dir, '*', '*.json')] + Dir['public/notes/*.json']
- @items = files.sort.collect do |file|
- item = JSON.parse(File.read(file))
- mention = mention(item['attributedTo'])
- following_path = File.join('public', 'following', mention + '.json')
- File.exists?(following_path) ? follow = 'unfollow' : follow = 'follow'
- nr += 1
- { :id => item['id'],
- :nr => nr,
- :parent => item['inReplyTo'],
- :file => file,
- :actor_url => item['attributedTo'],
- :mention => mention,
- :follow => follow,
- :content => item['content'],
- :attachment => item['attachment'],
- :indent => 0,
- :replies => []
- }
- end.compact
- @items.last[:nr] = @items.last[:nr] - 2 unless @items.empty?
+ def outbox(activity)
+ curl("-X POST -d #{activity.to_json}", File.join(SOCIAL_URL, 'outbox'))
end
def threads
- items
@threads = []
@items.each do |i|
- if i[:parent].nil? or @items.select{|it| it[:id] == i[:parent] }.empty?
+ i['indent'] = 0
+ i['replies'] = []
+ if i['inReplyTo'].nil? || @items.select { |it| it['id'] == i['inReplyTo'] }.empty?
@threads << i
else
- @items.select{|it| it[:id] == i[:parent] }.each do |it|
- i[:indent] = it[:indent] + 4
- it[:replies] << i
+ @items.select { |it| it['id'] == i['inReplyTo'] }.each do |it|
+ i['indent'] = it['indent'] + 4
+ it['replies'] << i
end
end
end
end
- def html item
- @item = item
- erb :item
- end
-
- def parse_follow follow
+ def parse_follow(follow)
case follow
when /^#/
- actor = "https://relay.fedi.buzz/tag/#{follow.sub(/^#/,'')}"
+ actor = "https://relay.fedi.buzz/tag/#{follow.sub(/^#/, '')}"
mention = follow
when /^http/
actor = follow
@@ -210,5 +180,4 @@ helpers do
end
[actor, mention]
end
-
end
diff --git a/helpers.rb b/helpers.rb
index a51ea3d..fdbd894 100644
--- a/helpers.rb
+++ b/helpers.rb
@@ -1,78 +1,82 @@
-helpers do
+# frozen_string_literal: true
- def curl ext, url
+require 'English'
+helpers do
+ def curl(ext, url)
+ p url
response = `/run/current-system/sw/bin/curl --fail-with-body -sSL #{ext} #{url}`
- $?.success? ? response : nil
+ $CHILD_STATUS.success? ? response : nil
end
- def fetch url, accept = 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'
+ # def fetch(url, accept = 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"')
+ def fetch(url, accept = 'application/activity+json')
response = curl("-H 'Accept: #{accept}'", url)
response ? JSON.parse(response) : nil
end
# https://github.com/mastodon/mastodon/blob/main/app/lib/request.rb
- def send_signed object#, url
-
+ # , url
+ def send_signed(object)
keypair = OpenSSL::PKey::RSA.new(File.read('private.pem'))
date = Time.now.utc.httpdate
body = object.to_json
- sha256 = OpenSSL::Digest::SHA256.new
- digest = "SHA-256=" + sha256.base64digest(body)
- jj object
- #(object['to'] + object['cc'] + object['bto'] + object['bcc']).uniq.each do |url|
- object['to'].uniq.each do |url|
- unless url == 'https://www.w3.org/ns/activitystreams#Public'
- host = URI.parse(url).host
- inbox = fetch(url)["inbox"]
- if inbox
- request_uri = URI(inbox).request_uri
+ sha256 = OpenSSL::Digest.new('SHA256')
+ digest = "SHA-256=#{sha256.base64digest(body)}"
+ recipients = [object['to'], object['cc'], object['bto'], object['bcc'], object['audience']].flatten.compact.uniq
+ recipients.each do |url|
+ next if url == 'https://www.w3.org/ns/activitystreams#Public'
+
+ host = URI.parse(url).host
+ inbox = fetch(url)['inbox']
+ if inbox
+ request_uri = URI(inbox).request_uri
- signed_string = "(request-target): post #{request_uri}\nhost: #{host}\ndate: #{date}\ndigest: #{digest}\ncontent-type: application/activity+json"
- signature = Base64.strict_encode64(keypair.sign(OpenSSL::Digest.new('SHA256'), signed_string))
- signed_header = 'keyId="' + ACTOR + '#main-key",algorithm="rsa-sha256",headers="(request-target) host date digest content-type",signature="' + signature + '"'
+ signed_string = "(request-target): post #{request_uri}\nhost: #{host}\ndate: #{date}\ndigest: #{digest}\ncontent-type: application/activity+json"
+ signature = Base64.strict_encode64(keypair.sign(OpenSSL::Digest.new('SHA256'), signed_string))
+ signed_header = "keyId=\"#{ACTOR}#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"#{signature}\""
- p curl("-X POST -H 'Content-Type: application/activity+json' -H 'Host: #{host}' -H 'Date: #{date}' -H 'Digest: #{digest}' -H 'Signature: #{signed_header}' -d '#{body}'", inbox)
- #$?.success?
- else
- p "No inbox for #{url}"
- end
+ curl(
+ "-X POST -H 'Content-Type: application/activity+json' -H 'Host: #{host}' -H 'Date: #{date}' -H 'Digest: #{digest}' -H 'Signature: #{signed_header}' -d '#{body}'", inbox
+ )
+ else
+ p "No inbox for #{url}"
end
end
-
end
def people
- File.read('cache/people.tsv').split("\n").collect {|l| l.chomp.split("\t")}
+ File.read('cache/people.tsv').split("\n").collect { |l| l.chomp.split("\t") }
end
- def mention actor
- person = people.select{|p| p[1] == actor}
+ def mention(actor)
+ person = people.select { |p| p[1] == actor }
if person.empty?
a = fetch(actor)
return nil unless a
- mention = "#{a["preferredUsername"]}@#{URI(actor).host}"
- File.open('cache/people.tsv','a'){|f| f.puts "#{mention}\t#{actor}"}
+
+ mention = "#{a['preferredUsername']}@#{URI(actor).host}"
+ File.open('cache/people.tsv', 'a') { |f| f.puts "#{mention}\t#{actor}" }
mention
else
person[0][0]
end
end
- def actor mention
+ def actor(mention)
mention = mention.sub(/^@/, '').chomp
- actors = people.select{|p| p[0] == mention}
+ actors = people.select { |p| p[0] == mention }
if actors.empty?
- user, server = mention.split("@")
- a = fetch("https://#{server}/.well-known/webfinger?resource=acct:#{mention}", "application/jrd+json")
+ _, server = mention.split('@')
+ a = fetch("https://#{server}/.well-known/webfinger?resource=acct:#{mention}", 'application/jrd+json')
return nil unless a
- actor = a["links"].select { |l|
- l["rel"] == "self"
- }[0]["href"]
- File.open('cache/people.tsv','a'){|f| f.puts "#{mention}\t#{actor}"}
+
+ actor = a['links'].select do |l|
+ l['rel'] == 'self'
+ end[0]['href']
+ File.open('cache/people.tsv', 'a') { |f| f.puts "#{mention}\t#{actor}" }
actor
else
actors[0][1]
end
end
-
end
diff --git a/server.rb b/server.rb
index 282c3e9..6379332 100644
--- a/server.rb
+++ b/server.rb
@@ -3,9 +3,12 @@
before '/inbox' do
request.body.rewind # in case someone already read it
@body = request.body.read
- @activity = JSON.parse @body
- @object = @activity['object']
- @object = fetch(@object) if @object.is_a?(String) && @object.match(/^http/)
+ unless @body.empty?
+ @activity = JSON.parse @body
+ @object = @activity['object']
+ @object = fetch(@object) if @object.is_a?(String) && @object.match(/^http/)
+ halt 400 unless @object
+ end
end
# client-server
@@ -17,6 +20,8 @@ end
# server-server
post '/inbox' do
verify!
+ # file = File.join INBOX, "#{SecureRandom.uuid}.json"
+ # File.open(file, 'w+') { |f| f.puts @activity.to_json }
type = @activity['type'].downcase.to_sym
respond_to?(type) ? send(type) : p("Unknown activity: #{type}")
end
@@ -31,7 +36,16 @@ get '/.well-known/webfinger' do
end
end
-['/outbox', '/following', '/followers'].each do |path|
+get '/outbox' do
+ ordered_collection(OUTBOX).to_json
+end
+
+get '/inbox' do
+ # protected!
+ ordered_collection(File.join(INBOX, 'note')).to_json
+end
+
+['/following', '/followers'].each do |path|
get path do
ordered_collection(File.join(PUBLIC_DIR, path)).to_json
end
@@ -44,11 +58,12 @@ end
helpers do
# https://github.com/mastodon/mastodon/blob/main/app/controllers/concerns/signature_verification.rb
def verify!
- # verify digest
+ # digest
sha256 = OpenSSL::Digest.new('SHA256')
digest = "SHA-256=#{sha256.base64digest(@body)}"
halt 403 unless digest == request.env['HTTP_DIGEST']
+ # signature
signature_params = {}
request.env['HTTP_SIGNATURE'].split(',').each do |pair|
k, v = pair.split('=')
@@ -60,9 +75,8 @@ helpers do
signature = Base64.decode64(signature_params['signature'])
actor = fetch key_id
- halt 200 if !actor && (@activity['type'] == 'Delete') # deleted users do not return actors
-
halt 403 unless actor
+
key = OpenSSL::PKey::RSA.new(actor['publicKey']['publicKeyPem'])
comparison = headers.split(' ').map do |signed_params_name|
@@ -79,10 +93,11 @@ helpers do
end
def create
+ return unless @object
return if object_exists?
File.open(object_file, 'w+') { |f| f.puts @object.to_json }
- return unless @object && @object['inReplyTo']
+ return unless @object['inReplyTo']
@object = fetch @object['inReplyTo']
create if @object
@@ -125,13 +140,13 @@ helpers do
'actor' => ACTOR,
'object' => @activity,
'to' => [@activity['actor']] }
- send_signed accept # , @activity['actor']
+ send_signed accept
end
+ # when "Like"
# when "Move"
# when "Add"
# when "Remove"
- # when "Like"
# when "Block"
def inbox
diff --git a/views/index.erb b/views/collection.erb
index b512911..4455b31 100644
--- a/views/index.erb
+++ b/views/collection.erb
@@ -9,14 +9,13 @@
<button><%= @alt_name %></button>
</form>
</h1>
- <% @threads.each do |item| %>
- <%= html item %>
- <% end %>
- <% if @dir == 'inbox' %>
- <form action='/delete' method='post'>
- <button>Delete all</button>
- </form>
+ <% @threads.each do |object|
+ @object = object %>
+ <%= erb :object %>
<% end %>
+ <form action='/delete' method='post'>
+ <button>Delete all</button>
+ </form>
</body>
<script>
const reply_buttons = document.querySelectorAll(".reply");
diff --git a/views/item.erb b/views/item.erb
deleted file mode 100644
index 8036b39..0000000
--- a/views/item.erb
+++ /dev/null
@@ -1,54 +0,0 @@
-<div style='margin-left:<%= @item[:indent] %>em' id='<%= @item[:nr] %>'>
- <b><a href='<%= @item[:actor_url] %>', target='_blank'><%= @item[:mention] %></a></b>&nbsp;
- <% if @item[:mention] != ACCOUNT %>
- <form action='/follow' method='post'>
- <input type='hidden' name='follow' value='<%= @item[:mention] %>' />
- <input type='hidden' name='redirect' value='/<%= @dir.sub('inbox','') %>#<%= @item[:nr] %>' />
- <button><%= @item[:follow].capitalize %></button>
- </form>
- &nbsp;
- <form action='/delete' method='post'>
- <input type='hidden' name='file' value='<%= @item[:file] %>' />
- <input type='hidden' name='redirect' value='/<%= @dir.sub('inbox','') %>#<%= @item[:nr] %>' />
- <button>Delete</button>
- </form>
- <% if @dir == 'inbox' %>
- &nbsp;
- <form action='/archive' method='post'>
- <input type='hidden' name='file' value='<%= @item[:file] %>' />
- <input type='hidden' name='redirect' value='/<%= @dir.sub('inbox','') %>#<%= @item[:nr] %>' />
- <button>Archive</button>
- </form>
- <% end %>
- <% end %>
- <%= @item[:content] %>
- <% if @item[:attachment]
- @item[:attachment].each do |att|
- case att['mediaType']
- when /audio/ %>
- <br><audio controls=''><source src='<%= att['url'] %>' type='<%= att['mediaType'] %>'></audio>
- <% when /image/ %>
- <br><a href='<%= att['url'] %>'><img src='<%= att['url'] %>'></a>
- <% when /video/ %>
- <br><video controls=''><source src='<%= att['url'] %>' type='<%= att['mediaType'] %>'></video>
- <% else %>
- <%= att %><br>
- <a href='<%= att['url'] %>'><%= att['url'] %></a>
- <% end %>
- <% end %>
- <% end %>
- <p>
- <button class="reply" data-index='<%= @item[:nr] %>'>Reply</button>
- <form action='/' method='post' id='form<%= @item[:nr] %>' style='display:none;' >
- <input type='hidden' name='to' value='<%= @item[:actor_url] %>' />
- <input type='hidden' name='inReplyTo' value='<%= @item[:id] %>' />
- <input type='hidden' name='redirect' value='/<%= @dir.sub('inbox','') %>#<%= @item[:nr] %>' />
- <textarea name='content'></textarea>
- <br>
- <button class="cancel" data-index='<%= @item[:nr] %>'>Cancel</button>
- <input type='submit' value='Send'>
- </form>
-</div>
-<% @item[:replies].each do |reply| %>
- <%= html reply %>
-<% end %>
diff --git a/views/object.erb b/views/object.erb
new file mode 100644
index 0000000..744a518
--- /dev/null
+++ b/views/object.erb
@@ -0,0 +1,57 @@
+
+<% mention = mention @object['attributedTo']
+ following_path = File.join(FOLLOWING, "#{mention}.json")
+ follow = File.exist?(following_path) ? 'unfollow' : 'follow'
+%>
+<div style='margin-left:<%= @object['indent']%>em' id='<%= @object['id'] %>'>
+ <b><a href='<%= @object['attributedTo'] %>', target='_blank'><%= mention %></a></b>&nbsp;
+ <form action='/<%= follow %>' method='post'>
+ <input type='hidden' name='follow' value='<%= @object['attributedTo'] %>' />
+ <input type='hidden' name='redirect' value='/#<%= @object['id'] %>' />
+ <button><%= follow.capitalize %></button>
+ </form>
+ &nbsp;
+ <form action='/delete' method='post'>
+ <input type='hidden' name='id' value='<%= @object['id'] %>' />
+ <input type='hidden' name='redirect' value='/#<%= @object['id'] %>' />
+ <button>Delete</button>
+ </form>
+ &nbsp;
+ <form action='/like' method='post'>
+ <input type='hidden' name='id' value='<%= @object['id'] %>' />
+ <input type='hidden' name='redirect' value='/#<%= @object['id'] %>' />
+ <button>Like</button>
+ </form>
+ <%= @object['content'] %>
+ <% if @object['attachment']
+ @object['attachment'].each do |att|
+ case att['mediaType']
+ when /audio/ %>
+ <br><audio controls=''><source src='<%= att['url'] %>' type='<%= att['mediaType'] %>'></audio>
+ <% when /image/ %>
+ <br><a href='<%= att['url'] %>'><img src='<%= att['url'] %>'></a>
+ <% when /video/ %>
+ <br><video controls=''><source src='<%= att['url'] %>' type='<%= att['mediaType'] %>'></video>
+ <% else %>
+ <%= att %><br>
+ <a href='<%= att['url'] %>'><%= att['url'] %></a>
+ <% end %>
+ <% end %>
+ <% end %>
+ <p>
+ <button class='reply' data-index='<%= @object['id'] %>'>Reply</button>
+ <form action='/' method='post' id='form<%= @object['id'] %>' style='display:none;' >
+ <input type='hidden' name='to' value='<%= @object['attributedTo'] %>' />
+ <input type='hidden' name='inReplyTo' value='<%= @object['id'] %>' />
+ <input type='hidden' name='redirect' value='/#<%= @object['id'] %>' />
+ <textarea name='content'></textarea>
+ <br>
+ <button class='cancel' data-index='<%= @object['id'] %>'>Cancel</button>
+ <input type='submit' value='Send'>
+ </form>
+</div>
+<% @object['replies'].each do |reply|
+ @object = reply %>
+ <%= erb :object %>
+<% end %>
+