summaryrefslogtreecommitdiff
path: root/activitypub.rb
diff options
context:
space:
mode:
Diffstat (limited to 'activitypub.rb')
-rw-r--r--activitypub.rb359
1 files changed, 3 insertions, 356 deletions
diff --git a/activitypub.rb b/activitypub.rb
index e250fe1..fd8583e 100644
--- a/activitypub.rb
+++ b/activitypub.rb
@@ -16,6 +16,7 @@ require 'uri'
require 'base64'
require 'digest/sha2'
require 'sinatra'
+require_relative 'helpers.rb'
USER = "pdp8"
WWW_DOMAIN = "pdp8.info"
@@ -31,359 +32,5 @@ set :session_secret, File.read(".secret").chomp
set :default_content_type, 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'
set :port, 9292
-# server-server
-post "/inbox" do
- verify!
- request.body.rewind # in case someone already read it
- body = request.body.read
- action = JSON.parse body
-
- case action["type"]
-
- when "Create"
- create action["object"]
-
- when "Delete"
- delete action["object"]
-
- when "Update"
- delete action["object"]
- create action["object"]
-
- when "Follow"
- File.open(File.join("public", "followers", mention(action["actor"]) + ".json"), "w+") { |f| f.puts body }
- accept = { "@context" => "https://www.w3.org/ns/activitystreams",
- "id" => File.join(SOCIAL_URL + "#accepts", SecureRandom.uuid),
- "type" => "Accept",
- "actor" => ACTOR,
- "object" => action }
- send_signed accept, action["actor"]
-
- when "Undo"
- o = action["object"]
- case o["type"]
- when "Follow"
- Dir["public/followers/*.json"].each do |follower|
- FileUtils.rm follower if JSON.parse(File.read(follower))["actor"] == o["actor"]
- end
- end
-
- when "Accept"
- o = action["object"]
- case o["type"]
- 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']}"
- p body
- end
-end
-
-# client-server
-post "/outbox" do
- protected!
- request.body.rewind # in case someone already read it
- body = request.body.read
- date = Time.now.strftime("%Y-%m-%dT%H:%M:%S")
- # TODO hashtags, replys
- outbox_path = File.join("public/outbox", date + ".json")
- object_path = File.join("public/objects", date + ".json")
- 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, object_path),
- "type" => "Note",
- "attributedTo" => ACTOR,
- "published" => date,
- "content" => "",
- "to" => ["https://www.w3.org/ns/activitystreams#Public"]
- },
- "published" => date,
- "to" => ["https://www.w3.org/ns/activitystreams#Public"]
- }
- recipients = []
- if /^@/.match body
- mentions, body = body.split("\n", 2)
- mentions.split(/, */).each do |m|
- recipients << actor(m.chomp)
- end
- end
- create["object"]["content"] = body.lines.select { |l| !l.empty? }.join("<br>")
- recipients += Dir[File.join("public/followers", "*.json")].collect { |f| JSON.parse(File.read(f))["actor"] }
- recipients.delete ACTOR
- recipients.uniq!
- create["object"]["to"] += recipients
- create["to"] += recipients
-
- File.open(outbox_path, "w+") { |f| f.puts create.to_json }
- File.open(object_path, "w+") { |f| f.puts create["object"].to_json }
-
- recipients.each { |r| send_signed create, r }
-end
-
-post "/archive" do
- protected!
- FileUtils.mv params['file'], "archive/"
- redirect to(params['redirect'])
-end
-
-post "/delete" do
- protected!
- FileUtils.rm params['file']
- redirect to(params['redirect'])
-end
-
-post "/delete_all" do
- protected!
- FileUtils.rm Dir["inbox/*.json"]
- redirect to("/")
-end
-
-post "/follow/*/*" do
- protected!
- mention = params['splat'][0]
- actor = actor(mention)
- follow = { "@context" => "https://www.w3.org/ns/activitystreams",
- "id" => File.join(SOCIAL_URL, "following", mention + ".json"),
- "type" => "Follow",
- "actor" => ACTOR,
- "object" => actor }
- send_signed follow, actor
- redirect to("/")
-end
-
-post "/unfollow/*" do
- protected!
- mention = params['splat'][0]
- actor = actor(mention)
- 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)) }
- send_signed undo, actor
- FileUtils.rm following_path
- redirect to("/")
- end
-end
-
-post "/login" do
- session["client"] = true if params["secret"] == File.read(".pwd").chomp
- redirect to("/")
-end
-
-# public
-get "/.well-known/webfinger" do
- if request["resource"] == "acct:#{ACCOUNT}"
- send_file "./public/webfinger", :type => "application/jrd+json"
- else
- halt 404
- end
-end
-
-get "/pdp8", :provides => 'html' do
- redirect 'https://pdp8.info'
-end
-
-get "/pdp8" do
- send_file "pdp8.json", :type => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'
-end
-
-["/outbox","/following","/followers"].each do |path|
- get path do
- ordered_collection(path).to_json
- end
-end
-
-# private
-["/", "/archive"].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
- threads
- erb :index
- end
-end
-
-helpers do
-
- def protected!
- redirect("/login.html") unless session['client']
- end
-
- def verify!
- # https://github.com/mastodon/mastodon/blob/main/app/controllers/concerns/signature_verification.rb
- # TODO verify digest
- begin
- signature_params = {}
- request.env["HTTP_SIGNATURE"].split(',').each do |pair|
- k, v = pair.split('=')
- signature_params[k] = v.gsub('"', '')
- end
-
- key_id = signature_params['keyId']
- headers = signature_params['headers']
- signature = Base64.decode64(signature_params['signature'])
-
- actor = fetch key_id
- key = OpenSSL::PKey::RSA.new(actor['publicKey']['publicKeyPem'])
-
- comparison = headers.split(' ').map do |signed_params_name|
- if signed_params_name == '(request-target)'
- '(request-target): post /inbox'
- elsif signed_params_name == 'content-type'
- "#{signed_params_name}: #{request.env["CONTENT_TYPE"]}"
- else
- "#{signed_params_name}: #{request.env["HTTP_" + signed_params_name.upcase]}"
- end
- end.join("\n")
-
- halt 400 unless key.verify(OpenSSL::Digest.new('SHA256'), signature, comparison)
- rescue => e
- p request.env["HTTP_SIGNATURE"], e
- halt 400
- end
- end
-
- def threads
- 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'],
- :indent => 2,
- :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 do |it|
- i[:indent] = it[:indent] + 2
- it[:replies] << i
- end
- end
- end
- end
-
- def html item
- @item = item
- erb :item
- 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 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
-
- def people
- File.read('inbox/people.tsv').split("\n").collect {|l| l.chomp.split("\t")}
- end
-
- 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
-
- 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"'
- p url
- response = `/run/current-system/sw/bin/curl --fail-with-body -sSL -H 'Accept: #{accept}' #{url}`
- halt 400 unless $?.success?
- JSON.parse(response)
- 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)
-
- sha256 = OpenSSL::Digest::SHA256.new
- body = object.to_json
- digest = "SHA-256=" + sha256.base64digest(body)
-
- 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
-
-end
+require_relative 'server.rb'
+require_relative 'client.rb'