summary refs log tree commit diff
diff options
context:
space:
mode:
authorpdp8 <pdp8@pdp8.info>2023-07-21 00:52:49 +0200
committerpdp8 <pdp8@pdp8.info>2023-07-21 00:52:49 +0200
commit711bf7f86daddd0209244f9640d8a3f27d958e3a (patch)
tree402b51a8a12d51173f954f9a10f1fe92f200e72f
parentedf5c00313b42308271fdad84a003b78a9483fc8 (diff)
inbox and outbox unified
-rw-r--r--activitypub.rb11
-rw-r--r--client.rb41
-rw-r--r--helpers.rb77
-rw-r--r--server.rb86
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