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 and @object.match(/^http/) end # client-server post '/outbox' do protected! #send_signed @activity end # server-server post "/inbox" do verify! type = @activity['type'].downcase.to_sym respond_to?(type) ? send(type) : p("Unknown activity: #{type}") end # public get "/.well-known/webfinger" do request["resource"] == "acct:#{ACCOUNT}" ? send_file("./public/webfinger", :type => "application/jrd+json") : halt(404) end ["/outbox","/following","/followers"].each do |path| get path do ordered_collection(File.join(PUBLIC_DIR,path)).to_json end end get '/tags/:tag' do |tag| ordered_collection(File.join(TAGS,tag)).to_json 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']) actor = fetch key_id if not actor and @activity["type"] == "Delete" # deleted users do not return actors halt 200 end 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 unless object_exists? File.open(object_file, "w+") { |f| f.puts @object.to_json } if @object and @object['inReplyTo'] @object = fetch @object['inReplyTo'] create if @object end end end def delete Dir["inbox/*/*.json"].each do |file| FileUtils.rm file if JSON.parse(File.read(file))['id'] == @object['id'] end end def update delete create end def announce create end def accept if @object['type'] == 'Follow' File.open(File.join(FOLLOWING, mention(@object['object'])+'.json'),'w+'){|f| f.puts @object.to_json} end end def undo if @object['type'] == 'Follow' Dir[File.join(FOLLOWERS, '*.json')].each do |follower| FileUtils.rm follower if JSON.parse(File.read(follower))["actor"] == @object["actor"] end end end def follow File.open(File.join(FOLLOWERS, mention(@activity['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' => @activity, 'to' => [ @activity['actor'] ] } send_signed accept#, @activity['actor'] end #when "Move" #when "Add" #when "Remove" #when "Like" #when "Block" def inbox Dir[File.join(INBOX,'*','*.json')].collect do |file| JSON.parse(File.read(file)) end.sort_by { |o| o["published"] } end def object_exists? not inbox.select{|o| o['id'] == @object['id']}.empty? end def object_file dir = File.join 'inbox', @object['type'].downcase FileUtils.mkdir_p dir File.join dir, "#{Time.now.strftime('%Y-%m-%dT%H:%M:%S.%N')}.json" end def ordered_collection dir posts = Dir[File.join(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