diff options
author | pdp8 <pdp8@pdp8.info> | 2023-06-30 15:42:14 +0200 |
---|---|---|
committer | pdp8 <pdp8@pdp8.info> | 2023-06-30 15:42:14 +0200 |
commit | 9d95a49ecd7e2f3e49b3462a281f2b86b406cf41 (patch) | |
tree | cd9695f2e22632e88eb710beeb041458e7ac3a71 | |
parent | e328b59ffc2476262dbd076d2478aaade78e649c (diff) |
inbox activities handled by dedicated functions
-rw-r--r-- | activitypub.rb | 10 | ||||
-rw-r--r-- | client.rb | 76 | ||||
-rw-r--r-- | helpers.rb | 32 | ||||
-rw-r--r-- | public/follow.html | 15 | ||||
-rw-r--r-- | public/pdp8 (renamed from pdp8.json) | 0 | ||||
-rw-r--r-- | server.rb | 169 | ||||
-rw-r--r-- | views/index.erb | 2 | ||||
-rw-r--r-- | views/item.erb | 6 |
8 files changed, 182 insertions, 128 deletions
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' @@ -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 @@ -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/public/follow.html b/public/follow.html new file mode 100644 index 0000000..7cbb37c --- /dev/null +++ b/public/follow.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<html lang='en'> + +<head> + <link rel='stylesheet' type='text/css' href='/style.css'> +</head> + +<body> + <form action='/follow' method='post'> + <input name='follow' /> + <input type='submit' name='button' value='Follow' /> + </form> +</body> + +</html>
\ No newline at end of file @@ -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' %> - <form action='delete_all' method='post'> + <form action='/delete' method='post'> <button>Delete all</button> </form> <% 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 @@ <div style='margin-left:<%= @item[:indent] %>em' id='<%= @item[:nr] %>'> <b><a href='<%= @item[:actor_url] %>', target='_blank'><%= @item[:mention] %></a></b> <% if @item[:mention] != ACCOUNT %> - <form action='<%= File.join @item[:follow], @item[:mention] %>' method='post'> + <form action='/follow' method='post'> + <input type='hidden' name='follow' value='<%= @item[:mention] %>' /> + <input type='hidden' name='redirect' value='/<%= @dir.sub('inbox','') %>#<%= @item[:nr] %>' /> <button><%= @item[:follow].capitalize %></button> </form> @@ -37,7 +39,7 @@ <% end %> <p> <button class="reply" data-index='<%= @item[:nr] %>'>Reply</button> - <form action='/outbox' method='post' id='form<%= @item[:nr] %>' style='display:none;' > + <form action='/' method='post' id='form<%= @item[:nr] %>' style='display:none;' > <input type='hidden' name='to' value='<%= @item[:actor_url] %>' /> <input type='hidden' name='inReplyTo' value='<%= @item[:id] %>' /> <input type='hidden' name='redirect' value='/<%= @dir.sub('inbox','') %>#<%= @item[:nr] %>' /> |