summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--activitypub.rb394
1 files changed, 207 insertions, 187 deletions
diff --git a/activitypub.rb b/activitypub.rb
index e6ab6da..f51ea3b 100644
--- a/activitypub.rb
+++ b/activitypub.rb
@@ -1,15 +1,20 @@
# TODO
-# read actor from people.csv
-# thread expansion
-# follow request confirmation
-# boost
+# server
+# fix failed follows
# federation
-# client post media
+# boost
+# thread expansion
+# include own posts in threads
+# remaining activities
# test with pleroma etc
+
+# client
+# post form
+# parse hashtags in post
+# client post media
require 'uri'
require 'base64'
require 'digest/sha2'
-require 'net/http'
require 'sinatra'
USER = "pdp8"
@@ -26,7 +31,8 @@ set :session_secret, File.read(".secret").chomp
set :default_content_type, 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'
set :port, 9292
-post "/inbox" do # server-server
+# server-server
+post "/inbox" do
verify!
request.body.rewind # in case someone already read it
body = request.body.read
@@ -68,7 +74,13 @@ post "/inbox" do # server-server
when "Follow"
File.open(File.join("public","following",mention(o['object'])+".json"),"w+"){|f| f.puts o.to_json}
end
+
#when "Announce"
+ #when "Move"
+ #when "Add"
+ #when "Remove"
+ #when "Like"
+ #when "Block"
else
p "Unknown action: #{action['type']}"
@@ -76,7 +88,8 @@ post "/inbox" do # server-server
end
end
-post "/outbox" do # client-server
+# client-server
+post "/outbox" do
protected!
request.body.rewind # in case someone already read it
body = request.body.read
@@ -173,6 +186,7 @@ post "/login" do
redirect to("/")
end
+# public
get "/.well-known/webfinger" do
if request["resource"] == "acct:#{ACCOUNT}"
send_file "./public/webfinger", :type => "application/jrd+json"
@@ -181,14 +195,12 @@ get "/.well-known/webfinger" do
end
end
-get "/archive", :provides => 'html' do
- protected!
- dir_html "archive"
+get "/pdp8", :provides => 'html' do
+ redirect 'https://pdp8.info'
end
-get "/", :provides => 'html' do
- protected!
- dir_html "inbox"
+get "/pdp8" do
+ send_file "pdp8.json"
end
["/outbox","/following","/followers"].each do |path|
@@ -197,6 +209,17 @@ end
end
end
+# private
+get "/archive", :provides => 'html' do
+ protected!
+ dir_html "archive"
+end
+
+get "/", :provides => 'html' do
+ protected!
+ dir_html "inbox"
+end
+
helpers do
def protected!
@@ -218,7 +241,6 @@ helpers do
signature = Base64.decode64(signature_params['signature'])
actor = fetch key_id
- halt 400 unless actor
key = OpenSSL::PKey::RSA.new(actor['publicKey']['publicKeyPem'])
comparison = headers.split(' ').map do |signed_params_name|
@@ -238,202 +260,200 @@ helpers do
halt 400
end
end
-end
-def dir_html dir
- nr = 0
- items = Dir[File.join(dir, '*.json')].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'],
- :replies => []
- }
- end.compact
- items.last[:nr] = items.last[:nr] - 2
- threads = []
- items.each do |i|
- if i[:parent].nil? or items.select{|it| it[:id] == i[:parent] }.empty?
- threads << i
- else
- items.select{|it| it[:id] == i[:parent] }.each{|it| it[:replies] << i}
+ def dir_html dir
+ nr = 0
+ items = Dir[File.join(dir, '*.json')].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'],
+ :replies => []
+ }
+ end.compact
+ items.last[:nr] = items.last[:nr] - 2
+ threads = []
+ items.each do |i|
+ if i[:parent].nil? or items.select{|it| it[:id] == i[:parent] }.empty?
+ threads << i
+ else
+ items.select{|it| it[:id] == i[:parent] }.each{|it| it[:replies] << i}
+ end
end
- end
- html="<!DOCTYPE html>
- <html lang='en'>
- <head>
- <link rel='stylesheet' type='text/css' href='/style.css'>
- </head>
- <body>
- <h1>#{dir}"
+ html="<!DOCTYPE html>
+ <html lang='en'>
+ <head>
+ <link rel='stylesheet' type='text/css' href='/style.css'>
+ </head>
+ <body>
+ <h1>#{dir}"
- if dir == "inbox"
- html << "<form action='archive' method='get'>
- <button>archive</button>
- </form>"
- elsif dir == "archive"
- html << "<form action='/' method='get'>
- <button>inbox</button>
- </form>"
- end
- html << " </h1> "
- threads.each do |item|
- html << item_html(item,dir)
+ if dir == "inbox"
+ html << "<form action='archive' method='get'>
+ <button>archive</button>
+ </form>"
+ elsif dir == "archive"
+ html << "<form action='/' method='get'>
+ <button>inbox</button>
+ </form>"
+ end
+ html << " </h1> "
+ threads.each do |item|
+ html << item_html(item,dir)
+ end
+ html << "
+ <form action='delete_all' method='post'>
+ <button>Delete all</button>
+ </form>
+ </body>
+ </html>" if dir == "inbox"
+ html
end
- html << "
- <form action='delete_all' method='post'>
- <button>Delete all</button>
- </form>
- </body>
- </html>" if dir == "inbox"
- html
-end
-def item_html item, dir, indent=2
- html = "
- <div style='margin-left:#{indent}em' id='#{item[:nr]}'>
- <b><a href='#{ item[:actor_url] }', target='_blank'>#{ item[:mention] }</a></b>&nbsp;
- <form action='#{ File.join item[:follow], item[:mention] }' method='post'>
- <button>#{ item[:follow].capitalize }</button>
- </form>
- &nbsp;
- "
- case dir
- when "inbox"
- html << "
- <form action='/archive' method='post'>
- <input type='hidden' name='file' id='file' value='#{item[:file]}' />
- <input type='hidden' name='anchor' id='anchor' value='#{item[:nr]}' />
- <button>Archive</button>
+ def item_html item, dir, indent=2
+ html = "
+ <div style='margin-left:#{indent}em' id='#{item[:nr]}'>
+ <b><a href='#{ item[:actor_url] }', target='_blank'>#{ item[:mention] }</a></b>&nbsp;
+ <form action='#{ File.join item[:follow], item[:mention] }' method='post'>
+ <button>#{ item[:follow].capitalize }</button>
</form>
+ &nbsp;
"
- when "archive"
- html << "
- <form action='/delete' method='post'>
- <input type='hidden' name='file' id='file' value='#{item[:file]}' />
- <input type='hidden' name='anchor' id='anchor' value='#{item[:nr]}' />
- <button>Delete</button>
- </form>
- "
- end
+ case dir
+ when "inbox"
+ html << "
+ <form action='/archive' method='post'>
+ <input type='hidden' name='file' id='file' value='#{item[:file]}' />
+ <input type='hidden' name='anchor' id='anchor' value='#{item[:nr]}' />
+ <button>Archive</button>
+ </form>
+ "
+ when "archive"
+ html << "
+ <form action='/delete' method='post'>
+ <input type='hidden' name='file' id='file' value='#{item[:file]}' />
+ <input type='hidden' name='anchor' id='anchor' value='#{item[:nr]}' />
+ <button>Delete</button>
+ </form>
+ "
+ end
- html << "
- #{ item[:content].gsub('<br />','') }"
- if item[:attachment]
- item[:attachment].each do |att|
- html << "<br>"
- case att['mediaType']
- when /audio/
- html << "<audio controls=''><source src='#{ att['url'] }' type='#{ att['mediaType'] }'></audio>"
- when /image/
- html << "<a href='#{ att['url'] }'><img src='#{ att['url'] }'></a>"
- when /video/
- html << "<video controls=''><source src='#{ att['url'] }' type='#{ att['mediaType'] }'></video>"
- else
- html << "#{ att }<br>
- <a href='#{ att['url'] }'>#{ att['url'] }</a>"
- end
+ html << "
+ #{ item[:content].gsub('<br />','') }"
+ if item[:attachment]
+ item[:attachment].each do |att|
+ html << "<br>"
+ case att['mediaType']
+ when /audio/
+ html << "<audio controls=''><source src='#{ att['url'] }' type='#{ att['mediaType'] }'></audio>"
+ when /image/
+ html << "<a href='#{ att['url'] }'><img src='#{ att['url'] }'></a>"
+ when /video/
+ html << "<video controls=''><source src='#{ att['url'] }' type='#{ att['mediaType'] }'></video>"
+ else
+ html << "#{ att }<br>
+ <a href='#{ att['url'] }'>#{ att['url'] }</a>"
+ end
+ end
end
- end
- html << "
- </div>"
- item[:replies].each do |r|
- html << item_html(r,dir,indent+4)
+ html << "
+ </div>"
+ item[:replies].each do |r|
+ html << item_html(r,dir,indent+4)
+ end
+
+ html
end
- html
-end
+ def delete object
+ Dir["inbox/*.json"].each do |doc|
+ FileUtils.rm doc if JSON.parse(File.read(doc))["id"] == object["id"]
+ end
+ end
-def delete object
- Dir["inbox/*.json"].each do |doc|
- FileUtils.rm doc if JSON.parse(File.read(doc))["id"] == object["id"]
+ def create object
+ unless object['type'] == 'Person'
+ doc = File.join("inbox", "#{Time.now.strftime('%Y-%m-%dT%H:%M:%S.%N')}.json")
+ File.open(doc, "w+") { |f| f.puts object.to_json }
+ end
end
-end
-def create object
- unless object['type'] == 'Person'
- doc = File.join("inbox", "#{Time.now.strftime('%Y-%m-%dT%H:%M:%S.%N')}.json")
- File.open(doc, "w+") { |f| f.puts object.to_json }
+ def people
+ File.read('inbox/people.tsv').split("\n").collect {|l| l.chomp.split("\t")}
end
-end
-def mention actor
- people = File.read('inbox/people.tsv').split("\n").collect {|l| l.chomp.split("\t")}
- person = people.select{|p| p[1] == actor}
- if person.empty?
- mention = "#{fetch(actor)["preferredUsername"]}@#{URI(actor).host}"
- File.open('inbox/people.tsv','a'){|f| f.puts "#{mention}\t#{actor}"}
- mention
- else
- person[0][0]
+ def mention actor
+ person = people.select{|p| p[1] == actor}
+ if person.empty?
+ mention = "#{fetch(actor)["preferredUsername"]}@#{URI(actor).host}"
+ File.open('inbox/people.tsv','a'){|f| f.puts "#{mention}\t#{actor}"}
+ mention
+ else
+ person[0][0]
+ end
end
-end
-def actor mention
- mention = mention.sub(/^@/, '').chomp
- user, server = mention.split("@")
- fetch("https://#{server}/.well-known/webfinger?resource=acct:#{mention}",
- "application/jrd+json")["links"].select { |l|
- l["rel"] == "self"
- }[0]["href"]
-end
+ def actor mention
+ mention = mention.sub(/^@/, '').chomp
+ actors = people.select{|p| p[0] == mention}
+ if actors.empty?
+ user, server = mention.split("@")
+ actor = fetch("https://#{server}/.well-known/webfinger?resource=acct:#{mention}",
+ "application/jrd+json")["links"].select { |l|
+ l["rel"] == "self"
+ }[0]["href"]
+ File.open('inbox/people.tsv','a'){|f| f.puts "#{mention}\t#{actor}"}
+ actor
+ else
+ actors[0][0]
+ end
+ end
-def fetch url, accept = 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', limit = 10
- uri = URI(url)
- http = Net::HTTP.new(uri.host, uri.port)
- http.use_ssl = true
- header = { 'Accept' => accept }
- request = Net::HTTP::Get.new(uri.request_uri, header)
- response = http.request(request)
- case response
- when Net::HTTPSuccess
- return JSON.parse(response.body)
- when Net::HTTPRedirection
- fetch response['location'], accept, limit-1
- else
- puts "#{url}: #{response.code}, #{response.message}"
- #halt 400
+ def fetch url, accept = 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'
+ response = `/run/current-system/sw/bin/curl --fail-with-body -sSL -H 'Accept: #{accept}' #{url}`
+ unless $?.success?
+ p url
+ halt 400
+ end
+ JSON.parse(response)
end
-end
-def ordered_collection dir
- posts = Dir[File.join("public",dir, "*.json")].collect { |f| JSON.parse(File.read f) }.sort_by { |o| o["published"] }
- {
- "@context" => "https://www.w3.org/ns/activitystreams",
- "summary" => "#{USER} #{dir}",
- "type" => "OrderedCollection",
- "totalItems" => posts.size,
- "orderedItems" => posts,
- }
-end
+ def ordered_collection dir
+ posts = Dir[File.join("public",dir, "*.json")].collect { |f| JSON.parse(File.read f) }.sort_by { |o| o["published"] }
+ {
+ "@context" => "https://www.w3.org/ns/activitystreams",
+ "summary" => "#{USER} #{dir}",
+ "type" => "OrderedCollection",
+ "totalItems" => posts.size,
+ "orderedItems" => posts,
+ }
+ end
-def send_signed object, url
- # https://github.com/mastodon/mastodon/blob/main/app/lib/request.rb
- keypair = OpenSSL::PKey::RSA.new(File.read('private.pem'))
- date = Time.now.utc.httpdate
- uri = URI.parse(url)
+ def send_signed object, url
+ # https://github.com/mastodon/mastodon/blob/main/app/lib/request.rb
+ keypair = OpenSSL::PKey::RSA.new(File.read('private.pem'))
+ date = Time.now.utc.httpdate
+ uri = URI.parse(url)
- sha256 = OpenSSL::Digest::SHA256.new
- body = object.to_json
- digest = "SHA-256=" + sha256.base64digest(body)
+ sha256 = OpenSSL::Digest::SHA256.new
+ body = object.to_json
+ digest = "SHA-256=" + sha256.base64digest(body)
- signed_string = "(request-target): post #{inbox uri}\nhost: #{uri.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 #{fetch(uri)["inbox"]}\nhost: #{uri.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 + '"'
- puts `/run/current-system/sw/bin/curl -i -X POST -H 'Content-Type: application/activity+json' -H 'Host: #{uri.host}' -H 'Date: #{date}' -H 'Digest: #{digest}' -H 'Signature: #{signed_header}' -d '#{body}' #{fetch(url)['inbox']}`
-end
+ puts `/run/current-system/sw/bin/curl -i -X POST -H 'Content-Type: application/activity+json' -H 'Host: #{uri.host}' -H 'Date: #{date}' -H 'Digest: #{digest}' -H 'Signature: #{signed_header}' -d '#{body}' #{fetch(url)['inbox']}`
+ end
-def inbox uri
- URI(fetch(uri)["inbox"]).request_uri
end