# server-server post "/inbox" do request.body.rewind # in case someone already read it @body = request.body.read @action = JSON.parse @body verify! 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" download @action["object"] #when "Move" #when "Add" #when "Remove" #when "Like" #when "Block" else p "Unknown @action: #{@action['type']}" p @body end end # public get "/.well-known/webfinger" do request["resource"] == "acct:#{ACCOUNT}" ? send_file("./public/webfinger", :type => "application/jrd+json") : halt(404) 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 helpers do # https://github.com/mastodon/mastodon/blob/main/app/controllers/concerns/signature_verification.rb def verify! # verify digest sha256 = OpenSSL::Digest::SHA256.new digest = "SHA-256=" + sha256.base64digest(@body) halt 403 unless digest == request.env["HTTP_DIGEST"] 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']) if @action["type"] == "Delete" # deleted users do not return actors delete @action["object"] halt 200 end jj @action actor = fetch key_id halt 403 unless actor 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 403 unless key.verify(OpenSSL::Digest.new('SHA256'), signature, comparison) 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 } if object['inReplyTo'] @dir = 'inbox' items if @items.select{|it| it[:id] == object['inReplyTo'] }.empty? download object['inReplyTo'] end end end end def download object_url object = fetch(object_url) object and object["type"] ? create(object) : p(object_url, object) 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 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 end