From 711bf7f86daddd0209244f9640d8a3f27d958e3a Mon Sep 17 00:00:00 2001 From: pdp8 Date: Fri, 21 Jul 2023 00:52:49 +0200 Subject: inbox and outbox unified --- activitypub.rb | 11 ++++---- client.rb | 41 ++++++++++++++++++++-------- helpers.rb | 77 ++++++++++++---------------------------------------- server.rb | 86 ++++++++++++++++++++++++++++++++++++++-------------------- 4 files changed, 111 insertions(+), 104 deletions(-) diff --git a/activitypub.rb b/activitypub.rb index 5148a27..f740406 100644 --- a/activitypub.rb +++ b/activitypub.rb @@ -11,12 +11,11 @@ PRIVATE_DIR = File.join(SOCIAL_DIR, 'private') OUTBOX_DIR = File.join(PUBLIC_DIR, 'outbox') TAGS_DIR = File.join(PUBLIC_DIR, 'tags') -INBOX = File.join(PRIVATE_DIR, 'inbox.json') +OLD_INBOX = File.join(PRIVATE_DIR, 'inbox.json') FOLLOWERS = File.join(PUBLIC_DIR, 'followers.json') FOLLOWING = File.join(PUBLIC_DIR, 'following.json') -OUTBOX = File.join(PUBLIC_DIR, 'outbox.json') -SHARED = File.join(PUBLIC_DIR, 'shared.json') -# VISITED = File.join(PRIVATE_DIR, 'visited') +# OLD_OUTBOX = File.join(PUBLIC_DIR, 'outbox.json') +# SHARED = File.join(PUBLIC_DIR, 'shared.json') USER = 'pdp8' SOCIAL_DOMAIN = 'social.pdp8.info' @@ -25,8 +24,10 @@ WEBFINGER = File.join(PUBLIC_DIR, MENTION + '.json') SOCIAL_URL = "https://#{SOCIAL_DOMAIN}" ACTOR = File.join(SOCIAL_URL, USER) -OUTBOX_URL = File.join(SOCIAL_URL, 'outbox') +# OLD_OUTBOX_URL = File.join(SOCIAL_URL, 'outbox') TAGS_URL = File.join(SOCIAL_URL, 'tags') +INBOX = { dir: File.join(SOCIAL_DIR, 'inbox') } +OUTBOX = { dir: File.join(SOCIAL_DIR, 'outbox'), url: File.join(SOCIAL_URL, 'outbox') } CONTENT_TYPE = 'application/activity+json' # CONTENT_TYPE = 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' diff --git a/client.rb b/client.rb index 386bce5..efc2f6d 100644 --- a/client.rb +++ b/client.rb @@ -3,7 +3,6 @@ # client-server post '/' do # TODO protected! - Time.now.strftime('%Y-%m-%dT%H:%M:%S.%N') recipients = public recipients << params[:to] @@ -116,10 +115,10 @@ end post '/share' do # TODO protected! - inbox = JSON.parse File.read(INBOX) - object = inbox['orderedItems'].find { |i| i['id'] == params['id'] } - update_collection SHARED, object - update_collection INBOX, object, true + # inbox = JSON.parse File.read(INBOX) + # object = inbox['orderedItems'].find { |i| i['id'] == params['id'] } + # update_collection SHARED, object + # update_collection INBOX, object, true recipients = public recipients << object['attributedTo'] outbox 'Announce', params['id'], recipients @@ -136,28 +135,48 @@ get '/' do redirect '/inbox' end -['/inbox', '/shared', '/outbox'].each do |path| +# ['/inbox', '/shared', '/outbox'].each do |path| +['/inbox', '/outbox'].each do |path| get path, provides: 'html' do protected! @dir = path.sub('/', '') - collection = JSON.parse(File.read(Kernel.const_get(@dir.upcase)))['orderedItems'].uniq + @collection = Dir[File.join(@dir, 'create', '*.json')].collect { |f| JSON.parse(File.read(f))['object'] } + @collection += Dir[File.join(@dir, 'announce', '*.json')].collect { |f| JSON.parse(File.read(f))['object'] } @threads = [] - collection.each do |object| + @collection.collect! do |object| + object = fetch(object) if object.is_a?(String) && object.match(/^http/) + object + end + @collection.each do |object| + add_parents object + end + @collection.each do |object| object['indent'] = 0 object['replies'] = [] - @threads << object if object['inReplyTo'].nil? || collection.select { |o| o['id'] == object['inReplyTo'] }.empty? + @threads << object if object['inReplyTo'].nil? || @collection.select { |o| o['id'] == object['inReplyTo'] }.empty? end - collection.each do |object| - collection.select { |o| o['id'] == object['inReplyTo'] }.each do |o| + @collection.each do |object| + @collection.select { |o| o['id'] == object['inReplyTo'] }.each do |o| object['indent'] = o['indent'] + 2 o['replies'] << object end end + @threads.sort_by! { |t| t['published'] } erb :collection end end helpers do + def add_parents(object) + return unless object['inReplyTo'] + + object = fetch object['inReplyTo'] + return unless object + + @collection << object unless @collection.collect { |o| o['id'] }.include? object['id'] + add_parents object + end + def protected! halt 403 unless session['client'] end diff --git a/helpers.rb b/helpers.rb index 0c668e9..45c58df 100644 --- a/helpers.rb +++ b/helpers.rb @@ -3,73 +3,32 @@ require 'English' helpers do - def outbox(type, object, recipients, add_recipients = false) - activity = { - '@context' => 'https://www.w3.org/ns/activitystreams', - 'type' => type, - 'actor' => ACTOR, - 'object' => object - } + # add date and id, save + def complete_and_save(activity) + box = activity['id'] ? INBOX : OUTBOX + date = Time.now.utc.iso8601 + activity['published'] = date if box == OUTBOX + basename = "#{activity['published']}_#{mention(activity['actor'])}.json" + activity_rel_path = File.join(activity['type'].downcase, basename) + activity_path = File.join(box[:dir], activity_rel_path) + activity['id'] = File.join(box[:url], activity_rel_path) if box == OUTBOX - now = Time.now - date = now.strftime('%Y-%m-%dT%H:%M:%S.%N') - httpdate = now.utc.httpdate - basename = "#{date}.json" - activity_rel_path = File.join(type.downcase, basename) - activity_path = File.join(OUTBOX_DIR, activity_rel_path) - activity['id'] = File.join(OUTBOX_URL, activity_rel_path) - activity['published'] = httpdate - # assumes that recipient collections have been expanded by sender - # put all recipients into 'to', avoid 'cc' 'bto' 'bcc' 'audience' !! - activity['to'] = recipients if add_recipients - - # save object if activity['object'] && activity['object']['type'] && !activity['object']['id'] - + # save object object = activity['object'] object['@context'] = 'https://www.w3.org/ns/activitystreams' - object_rel_path = File.join activity['object']['type'].downcase, basename - object_path = File.join OUTBOX_DIR, object_rel_path - object['id'] = File.join OUTBOX_URL, object_rel_path - object['published'] = httpdate + object_rel_path = File.join 'object', activity['object']['type'].downcase, basename + if box == OUTBOX + object['id'] = File.join box[:url], object_rel_path + object['published'] = date + end + object_path = File.join box[:dir], object_rel_path FileUtils.mkdir_p File.dirname(object_path) File.open(object_path, 'w+') { |f| f.puts object.to_json } end # save activity FileUtils.mkdir_p File.dirname(activity_path) File.open(activity_path, 'w+') { |f| f.puts activity.to_json } - update_collection OUTBOX, activity['id'] if %w[Create Announce].include?(type) - - # send - # https://github.com/mastodon/mastodon/blob/main/app/lib/request.rb - keypair = OpenSSL::PKey::RSA.new(File.read('private.pem')) - body = activity.to_json - sha256 = OpenSSL::Digest.new('SHA256') - digest = "SHA-256=#{sha256.base64digest(body)}" - - inboxes = [] - recipients.uniq.each do |url| - next if [ACTOR, 'https://www.w3.org/ns/activitystreams#Public'].include? url - - actor = fetch url - next unless actor - - if actor['endpoints'] and actor['endpoints']['sharedInbox'] - inboxes << actor['endpoints']['sharedInbox'] - elsif actor['inbox'] - inboxes << actor['inbox'] - end - end - inboxes.compact.uniq.each do |inbox| - uri = URI(inbox) - string = "(request-target): post #{uri.request_uri}\nhost: #{uri.host}\ndate: #{httpdate}\ndigest: #{digest}\ncontent-type: application/activity+json" - signature = Base64.strict_encode64(keypair.sign(OpenSSL::Digest.new('SHA256'), string)) - signed_header = "keyId=\"#{ACTOR}#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"#{signature}\"" - - curl( - "-X POST -H 'Content-Type: application/activity+json' -H 'Host: #{uri.host}' -H 'Date: #{httpdate}' -H 'Digest: #{digest}' -H 'Signature: #{signed_header}' -d '#{body}'", inbox - ) - end activity end @@ -154,11 +113,11 @@ helpers do end def people - File.read('private/people.tsv').split("\n").collect { |l| l.chomp.split("\t") } + File.read('public/people.tsv').split("\n").collect { |l| l.chomp.split("\t") } end def cache(mention, actor, a) sharedInbox = a['endpoints']['sharedInbox'] if a['endpoints'] && a['endpoints']['sharedInbox'] - File.open('private/people.tsv', 'a') { |f| f.puts "#{mention}\t#{actor}\t#{sharedInbox}" } + File.open('public/people.tsv', 'a') { |f| f.puts "#{mention}\t#{actor}\t#{sharedInbox}" } end end diff --git a/server.rb b/server.rb index 49204a3..f7e9b82 100644 --- a/server.rb +++ b/server.rb @@ -11,14 +11,11 @@ post '/inbox' do p @body halt 400 end + halt 501 if @activity['actor'] and @activity['type'] == 'Delete' # deleted actors return 403 => verification error + verify! # unless type == :accept # pixelfed sends unsigned accept activities??? + complete_and_save(@activity) type = @activity['type'].downcase.to_sym - p type - halt 501 unless respond_to?(type) - @object = @activity['object'] - @object = fetch(@object) if @object.is_a?(String) && @object.match(/^http/) - halt 400 unless @object - verify! unless type == :accept # pixelfed sends unsigned accept activities??? - send(type) + send(type) if %i[follow accept undo].include? type halt 200 end @@ -28,43 +25,30 @@ get '/.well-known/webfinger' do send_file(WEBFINGER, type: 'application/jrd+json') end -['/pdp8', '/following', '/followers', '/outbox', '/shared'].each do |path| +get '/pdp8' do + send_file(File.join(PUBLIC_DIR, 'pdp8.json'), type: CONTENT_TYPE) +end + +['/following', '/followers'].each do |path| get path do send_file(File.join(PUBLIC_DIR, path) + '.json', type: CONTENT_TYPE) end end helpers do - def create - return unless @object - - # return if File.readlines(VISITED).collect { |l| l.chomp }.include? @object['id'] - - # File.open(VISITED, 'a+') { |f| f.puts @object['id'] } - update_collection INBOX, @object - return unless @object['inReplyTo'] - - @object = fetch @object['inReplyTo'] - create if @object - end - - def announce - create - end - def follow update_collection FOLLOWERS, @activity['actor'] outbox 'Accept', @activity, [@activity['actor']] end def accept - halt 501 unless @object['type'] == 'Follow' - update_collection FOLLOWING, @object['object'] + halt 501 unless @activity['object']['type'] == 'Follow' + update_collection FOLLOWING, @activity['object']['object'] end def undo - halt 501 unless @object['type'] == 'Follow' - update_collection FOLLOWERS, @object['actor'], true + halt 501 unless @activity['object']['type'] == 'Follow' + update_collection FOLLOWERS, @activity['object']['actor'], true end # https://github.com/mastodon/mastodon/blob/main/app/controllers/concerns/signature_verification.rb @@ -102,4 +86,48 @@ helpers do halt 403 unless key.verify(OpenSSL::Digest.new('SHA256'), signature, comparison) end + + def outbox(type, object, recipients) + # add date and id, save + activity = complete_and_save({ + '@context' => 'https://www.w3.org/ns/activitystreams', + 'type' => type, + 'actor' => ACTOR, + 'object' => object, + 'to' => recipients + }) + + # send + # https://github.com/mastodon/mastodon/blob/main/app/lib/request.rb + keypair = OpenSSL::PKey::RSA.new(File.read('private.pem')) + body = activity.to_json + sha256 = OpenSSL::Digest.new('SHA256') + digest = "SHA-256=#{sha256.base64digest(body)}" + + inboxes = [] + recipients.uniq.each do |url| + next if [ACTOR, 'https://www.w3.org/ns/activitystreams#Public'].include? url + + actor = fetch url + next unless actor + + if actor['endpoints'] and actor['endpoints']['sharedInbox'] + inboxes << actor['endpoints']['sharedInbox'] + elsif actor['inbox'] + inboxes << actor['inbox'] + end + end + + inboxes.compact.uniq.each do |inbox| + uri = URI(inbox) + string = "(request-target): post #{uri.request_uri}\nhost: #{uri.host}\ndate: #{httpdate}\ndigest: #{digest}\ncontent-type: application/activity+json" + signature = Base64.strict_encode64(keypair.sign(OpenSSL::Digest.new('SHA256'), string)) + signed_header = "keyId=\"#{ACTOR}#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"#{signature}\"" + + curl( + "-X POST -H 'Content-Type: application/activity+json' -H 'Host: #{uri.host}' -H 'Date: #{httpdate}' -H 'Digest: #{digest}' -H 'Signature: #{signed_header}' -d '#{body}'", inbox + ) + end + activity + end end -- cgit v1.2.3