# frozen_string_literal: true before '/inbox' do request.body.rewind # in case someone already read it @body = request.body.read 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 post '/outbox' do protected! # send_signed @activity 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 # 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 '/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 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! # 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('=') 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 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 return unless @object return if object_exists? File.open(object_file, 'w+') { |f| f.puts @object.to_json } return unless @object['inReplyTo'] @object = fetch @object['inReplyTo'] create if @object 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 return unless @object['type'] == 'Follow' File.open(File.join(FOLLOWING, "#{mention(@object['object'])}.json"), 'w+') { |f| f.puts @object.to_json } end def undo return unless @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 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 end # when "Like" # when "Move" # when "Add" # when "Remove" # 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? !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