From 9d95a49ecd7e2f3e49b3462a281f2b86b406cf41 Mon Sep 17 00:00:00 2001 From: pdp8 Date: Fri, 30 Jun 2023 15:42:14 +0200 Subject: inbox activities handled by dedicated functions --- activitypub.rb | 10 +++- client.rb | 76 ++++++++++++++---------- helpers.rb | 32 ++++++---- pdp8.json | 39 ------------- public/follow.html | 15 +++++ public/pdp8 | 39 +++++++++++++ server.rb | 169 +++++++++++++++++++++++++++-------------------------- views/index.erb | 2 +- views/item.erb | 6 +- 9 files changed, 221 insertions(+), 167 deletions(-) delete mode 100644 pdp8.json create mode 100644 public/follow.html create mode 100644 public/pdp8 diff --git a/activitypub.rb b/activitypub.rb index 9b63b46..9a1b9d4 100644 --- a/activitypub.rb +++ b/activitypub.rb @@ -7,6 +7,13 @@ USER = "pdp8" WWW_DOMAIN = "pdp8.info" WWW_URL = "https://#{WWW_DOMAIN}" SOCIAL_DOMAIN = "social.#{WWW_DOMAIN}" +SOCIAL_DIR = '/srv/social/' +INBOX = File.join(SOCIAL_DIR, 'inbox') +PUBLIC_DIR = File.join(SOCIAL_DIR, 'public') +OUTBOX = File.join(PUBLIC_DIR, 'outbox') +FOLLOWERS = File.join(PUBLIC_DIR, 'followers') +FOLLOWING = File.join(PUBLIC_DIR, 'following') +TAGS = File.join(PUBLIC_DIR, 'tags') ACCOUNT = "#{USER}@#{SOCIAL_DOMAIN}" SOCIAL_URL = "https://#{SOCIAL_DOMAIN}" @@ -14,7 +21,8 @@ ACTOR = File.join(SOCIAL_URL, USER) enable :sessions set :session_secret, File.read(".secret").chomp -set :default_content_type, 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' +#set :default_content_type, 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' +set :default_content_type, 'application/activity+json' set :port, 9292 require_relative 'helpers.rb' diff --git a/client.rb b/client.rb index 7c58f85..abf565d 100644 --- a/client.rb +++ b/client.rb @@ -1,5 +1,5 @@ # client-server -post "/outbox" do +post "/" do protected! date = Time.now.strftime("%Y-%m-%dT%H:%M:%S") outbox_path = File.join("public/outbox", date + ".json") @@ -65,10 +65,16 @@ post "/outbox" do File.open(outbox_path, "w+") { |f| f.puts create.to_json } File.open(notes_path, "w+") { |f| f.puts create["object"].to_json } + tag.each do |t| + dir = File.join('public','tags',t['name'].sub('#','')) + FileUtils.mkdir_p dir + FileUtils.ln_s File.join('/srv/social/',notes_path), dir + end - recipients.delete "https://www.w3.org/ns/activitystreams#Public" - recipients.each { |r| send_signed create, r } - redirect to(params['redirect']) + #recipients.delete "https://www.w3.org/ns/activitystreams#Public" + #recipients.each { |r| send_signed create, r } + send_signed create #, r } + redirect params['redirect'] end post "/archive" do @@ -77,53 +83,45 @@ post "/archive" do redirect to(params['redirect']) end -post "/delete" do +post "/delete" do # delete not supported by html forms protected! - FileUtils.rm params['file'] - redirect to(params['redirect']) + params['file'] ? FileUtils.rm_f(params['file']) : FileUtils.rm_f(Dir["inbox/*.json"]) + params['redirect'] ? redirect(params['redirect']) : redirect('/') end -post "/delete_all" do +post "/follow" do protected! - FileUtils.rm Dir["inbox/*.json"] - redirect to("/") -end - -post "/follow/*" do - protected! - mention = params['splat'][0] - actor = actor(mention) - return 502 unless actor + actor, mention = parse_follow params['follow'] follow = { "@context" => "https://www.w3.org/ns/activitystreams", - "id" => File.join(SOCIAL_URL, "following", mention + ".json"), + "id" => File.join(SOCIAL_URL, "following", mention+ ".json"), "type" => "Follow", "actor" => ACTOR, - "object" => actor } - send_signed follow, actor - redirect to("/") + "object" => actor, + 'to' => [ actor ] } + send_signed follow#, actor + redirect "/" end -post "/unfollow/*" do +post "/unfollow" do protected! - mention = params['splat'][0] - actor = actor(mention) - return 502 unless actor + actor, mention = parse_follow params['follow'] following_path = File.join("public", "following", mention + ".json") if File.exists?(following_path) undo = { "@context" => "https://www.w3.org/ns/activitystreams", "id" => File.join(SOCIAL_URL + "#undo", SecureRandom.uuid), "type" => "Undo", "actor" => ACTOR, - "object" => JSON.parse(File.read(following_path)) } - send_signed undo, actor + "object" => JSON.parse(File.read(following_path)), + 'to' => [ actor ] } + send_signed undo#, actor FileUtils.rm following_path - redirect to("/") + redirect "/" end end post "/login" do session["client"] = true if OpenSSL::Digest::SHA256.base64digest(params["secret"]) == File.read(".digest").chomp - redirect to("/") + redirect "/" end # private @@ -152,7 +150,9 @@ helpers do def items nr = 0 - files = Dir[File.join(@dir, '*.json')] + Dir['public/notes/*.json'] + p @dir + p Dir[File.join(@dir, '*', '*.json')]# + Dir['public/notes/*.json'] + files = Dir[File.join(@dir, '*', '*.json')] + Dir['public/notes/*.json'] @items = files.sort.collect do |file| item = JSON.parse(File.read(file)) mention = mention(item['attributedTo']) @@ -195,4 +195,20 @@ helpers do erb :item end + def parse_follow follow + case follow + when /^#/ + actor = "https://relay.fedi.buzz/tag/#{follow.sub(/^#/,'')}" + mention = follow + when /^http/ + actor = follow + mention = mention actor + when /^@*\w+@\w+/ + mention = follow + actor = actor(follow) + return 502 unless actor + end + [actor, mention] + end + end diff --git a/helpers.rb b/helpers.rb index 68aabe4..a51ea3d 100644 --- a/helpers.rb +++ b/helpers.rb @@ -1,8 +1,7 @@ helpers do def curl ext, url - #p "/run/current-system/sw/bin/curl -fsSL #{ext} #{url}" - response = `/run/current-system/sw/bin/curl -fsSL #{ext} #{url}` + response = `/run/current-system/sw/bin/curl --fail-with-body -sSL #{ext} #{url}` $?.success? ? response : nil end @@ -12,24 +11,33 @@ helpers do end # https://github.com/mastodon/mastodon/blob/main/app/lib/request.rb - def send_signed object, url + def send_signed object#, url keypair = OpenSSL::PKey::RSA.new(File.read('private.pem')) date = Time.now.utc.httpdate body = object.to_json sha256 = OpenSSL::Digest::SHA256.new digest = "SHA-256=" + sha256.base64digest(body) - host = URI.parse(url).host - inbox = fetch(url)["inbox"] - return false unless inbox - request_uri = URI(inbox).request_uri + jj object + #(object['to'] + object['cc'] + object['bto'] + object['bcc']).uniq.each do |url| + object['to'].uniq.each do |url| + unless url == 'https://www.w3.org/ns/activitystreams#Public' + host = URI.parse(url).host + inbox = fetch(url)["inbox"] + if inbox + request_uri = URI(inbox).request_uri - signed_string = "(request-target): post #{request_uri}\nhost: #{host}\ndate: #{date}\ndigest: #{digest}\ncontent-type: application/activity+json" - signature = Base64.strict_encode64(keypair.sign(OpenSSL::Digest.new('SHA256'), signed_string)) - signed_header = 'keyId="' + ACTOR + '#main-key",algorithm="rsa-sha256",headers="(request-target) host date digest content-type",signature="' + signature + '"' + signed_string = "(request-target): post #{request_uri}\nhost: #{host}\ndate: #{date}\ndigest: #{digest}\ncontent-type: application/activity+json" + signature = Base64.strict_encode64(keypair.sign(OpenSSL::Digest.new('SHA256'), signed_string)) + signed_header = 'keyId="' + ACTOR + '#main-key",algorithm="rsa-sha256",headers="(request-target) host date digest content-type",signature="' + signature + '"' - p curl("-X POST -H 'Content-Type: application/activity+json' -H 'Host: #{host}' -H 'Date: #{date}' -H 'Digest: #{digest}' -H 'Signature: #{signed_header}' -d '#{body}'", inbox) - $?.success? + p curl("-X POST -H 'Content-Type: application/activity+json' -H 'Host: #{host}' -H 'Date: #{date}' -H 'Digest: #{digest}' -H 'Signature: #{signed_header}' -d '#{body}'", inbox) + #$?.success? + else + p "No inbox for #{url}" + end + end + end end diff --git a/pdp8.json b/pdp8.json deleted file mode 100644 index fafc4e0..0000000 --- a/pdp8.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "@context": [ - "https://www.w3.org/ns/activitystreams" - ], - "id": "https://social.pdp8.info/pdp8", - "type": "Person", - "preferredUsername": "pdp8", - "name": "pdp8", - "inbox": "https://social.pdp8.info/inbox", - "outbox": "https://social.pdp8.info/outbox", - "following": "https://social.pdp8.info/following", - "followers": "https://social.pdp8.info/followers", - "icon": { - "type": "Image", - "url": "https://pdp8.info/pdp8.png" - }, - "attachment": [ - { - "type": "PropertyValue", - "name": "Web", - "value": "pdp8.info" - }, - { - "type": "PropertyValue", - "name": "Fediverse", - "value": "@pdp8@social.pdp8.info" - }, - { - "type": "PropertyValue", - "name": "Matrix", - "value": "@pdp8:matrix.pdp8.info" - } - ], - "publicKey": { - "id": "https://social.pdp8.info/pdp8#main-key", - "owner": "https://social.pdp8.info/pdp8", - "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArDawzSl+XcJ+96sIrx+E\nsDoUQzSvoKazCgw7qOMaOGi7XxJ8riBvdRBlJ4zOEfQaxcaQgGn5JntOofqkeWvk\nIykOAzYfwY6HoUm7i1eZME2quO+CkMMq9SX9/DOqggOYtiVC9DX5FxXe5YHK7Q/n\nbo1iB6rgVS43wT0PnI6uduY4cUlvhRkX4Iht0N1GTrBlGKloRQ96KTzp+U9xF7bp\nKO87Y4yftv+d6L3ZZBfTRgWOtDXG8E4Vdvsq0aPQNBtazq0fwtBbk2G4mZtCMqyT\nvLZh8w+YPn1ICoQsKukU/q7eG29UJCz/QdZndkuv5iIm+H/c8gicGllw9rNQP2G0\nBQIDAQAB\n-----END PUBLIC KEY-----\n" - } -} diff --git a/public/follow.html b/public/follow.html new file mode 100644 index 0000000..7cbb37c --- /dev/null +++ b/public/follow.html @@ -0,0 +1,15 @@ + + + + + + + + +
+ + +
+ + + \ No newline at end of file diff --git a/public/pdp8 b/public/pdp8 new file mode 100644 index 0000000..fafc4e0 --- /dev/null +++ b/public/pdp8 @@ -0,0 +1,39 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams" + ], + "id": "https://social.pdp8.info/pdp8", + "type": "Person", + "preferredUsername": "pdp8", + "name": "pdp8", + "inbox": "https://social.pdp8.info/inbox", + "outbox": "https://social.pdp8.info/outbox", + "following": "https://social.pdp8.info/following", + "followers": "https://social.pdp8.info/followers", + "icon": { + "type": "Image", + "url": "https://pdp8.info/pdp8.png" + }, + "attachment": [ + { + "type": "PropertyValue", + "name": "Web", + "value": "pdp8.info" + }, + { + "type": "PropertyValue", + "name": "Fediverse", + "value": "@pdp8@social.pdp8.info" + }, + { + "type": "PropertyValue", + "name": "Matrix", + "value": "@pdp8:matrix.pdp8.info" + } + ], + "publicKey": { + "id": "https://social.pdp8.info/pdp8#main-key", + "owner": "https://social.pdp8.info/pdp8", + "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArDawzSl+XcJ+96sIrx+E\nsDoUQzSvoKazCgw7qOMaOGi7XxJ8riBvdRBlJ4zOEfQaxcaQgGn5JntOofqkeWvk\nIykOAzYfwY6HoUm7i1eZME2quO+CkMMq9SX9/DOqggOYtiVC9DX5FxXe5YHK7Q/n\nbo1iB6rgVS43wT0PnI6uduY4cUlvhRkX4Iht0N1GTrBlGKloRQ96KTzp+U9xF7bp\nKO87Y4yftv+d6L3ZZBfTRgWOtDXG8E4Vdvsq0aPQNBtazq0fwtBbk2G4mZtCMqyT\nvLZh8w+YPn1ICoQsKukU/q7eG29UJCz/QdZndkuv5iIm+H/c8gicGllw9rNQP2G0\nBQIDAQAB\n-----END PUBLIC KEY-----\n" + } +} diff --git a/server.rb b/server.rb index 2c24bb9..809538b 100644 --- a/server.rb +++ b/server.rb @@ -1,59 +1,22 @@ -# server-server -post "/inbox" do +before '/inbox' do request.body.rewind # in case someone already read it @body = request.body.read - @action = JSON.parse @body - verify! - - case @action["type"] - - when "Create" - create @action["object"] - - when "Delete" - delete @action["object"] - - when "Update" - delete @action["object"] - create @action["object"] - - when "Follow" - File.open(File.join("public", "followers", mention(@action["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" => @action } - send_signed accept, @action["actor"] - - when "Undo" - o = @action["object"] - case o["type"] - when "Follow" - Dir["public/followers/*.json"].each do |follower| - FileUtils.rm follower if JSON.parse(File.read(follower))["actor"] == o["actor"] - end - end - - when "Accept" - o = @action["object"] - case o["type"] - when "Follow" - File.open(File.join("public","following",mention(o['object'])+".json"),"w+"){|f| f.puts o.to_json} - end + @activity = JSON.parse @body + @object = @activity['object'] + @object = fetch(@object) if @object.is_a? String and @object.match(/^http/) +end - when "Announce" - download @action["object"] - #when "Move" - #when "Add" - #when "Remove" - #when "Like" - #when "Block" +# client-server +post '/outbox' do + protected! + #send_signed @activity +end - else - p "Unknown @action: #{@action['type']}" - p @body - end +# server-server +post "/inbox" do + verify! + type = @activity['type'].downcase.to_sym + respond_to?(type) ? send(type) : p("Unknown activity: #{type}") end # public @@ -61,20 +24,16 @@ get "/.well-known/webfinger" do request["resource"] == "acct:#{ACCOUNT}" ? send_file("./public/webfinger", :type => "application/jrd+json") : halt(404) end -get "/pdp8", :provides => 'html' do - redirect 'https://pdp8.info' -end - -get "/pdp8" do - send_file "pdp8.json", :type => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' -end - ["/outbox","/following","/followers"].each do |path| get path do - ordered_collection(path).to_json + 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 @@ -95,13 +54,11 @@ helpers do headers = signature_params['headers'] signature = Base64.decode64(signature_params['signature']) - if @action["type"] == "Delete" # deleted users do not return actors - delete @action["object"] + actor = fetch key_id + if not actor and @activity["type"] == "Delete" # deleted users do not return actors halt 200 end - jj @action - actor = fetch key_id halt 403 unless actor key = OpenSSL::PKey::RSA.new(actor['publicKey']['publicKeyPem']) @@ -118,33 +75,81 @@ helpers do halt 403 unless key.verify(OpenSSL::Digest.new('SHA256'), signature, comparison) end - def create object - unless object['type'] == 'Person' - doc = File.join("inbox", "#{Time.now.strftime('%Y-%m-%dT%H:%M:%S.%N')}.json") - File.open(doc, "w+") { |f| f.puts object.to_json } - if object['inReplyTo'] - @dir = 'inbox' - items - if @items.select{|it| it[:id] == object['inReplyTo'] }.empty? - download object['inReplyTo'] - 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 download object_url - object = fetch(object_url) - object and object["type"] ? create(object) : p(object_url, object) + 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 delete object - Dir["inbox/*.json"].each do |doc| - FileUtils.rm doc if JSON.parse(File.read(doc))["id"] == object["id"] + 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("public",dir, "*.json")].collect { |f| JSON.parse(File.read f) }.sort_by { |o| o["published"] } + 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}", diff --git a/views/index.erb b/views/index.erb index 0021ec9..b512911 100644 --- a/views/index.erb +++ b/views/index.erb @@ -13,7 +13,7 @@ <%= html item %> <% end %> <% if @dir == 'inbox' %> -
+
<% end %> diff --git a/views/item.erb b/views/item.erb index ebc632e..8036b39 100644 --- a/views/item.erb +++ b/views/item.erb @@ -1,7 +1,9 @@
<%= @item[:mention] %>  <% if @item[:mention] != ACCOUNT %> -
+ + + #<%= @item[:nr] %>' />
  @@ -37,7 +39,7 @@ <% end %>

-