summaryrefslogtreecommitdiff
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