From 5af8d78e195c7479769240b32703d5b76843db4d Mon Sep 17 00:00:00 2001 From: pdp8 Date: Sat, 1 Jul 2023 00:57:48 +0200 Subject: initial refactoring of client.rb --- activitypub.rb | 2 +- client.rb | 201 ++++++++++++++++++++++----------------------------- helpers.rb | 82 +++++++++++---------- server.rb | 35 ++++++--- views/collection.erb | 38 ++++++++++ views/index.erb | 39 ---------- views/item.erb | 54 -------------- views/object.erb | 57 +++++++++++++++ 8 files changed, 249 insertions(+), 259 deletions(-) create mode 100644 views/collection.erb delete mode 100644 views/index.erb delete mode 100644 views/item.erb create mode 100644 views/object.erb diff --git a/activitypub.rb b/activitypub.rb index ae3c270..eefae9b 100644 --- a/activitypub.rb +++ b/activitypub.rb @@ -28,5 +28,5 @@ set :default_content_type, 'application/activity+json' set :port, 9292 require_relative 'helpers' -require_relative 'server' require_relative 'client' +require_relative 'server' diff --git a/client.rb b/client.rb index abf565d..b62f139 100644 --- a/client.rb +++ b/client.rb @@ -1,28 +1,30 @@ +# frozen_string_literal: true + # client-server -post "/" do +post '/' do protected! - date = Time.now.strftime("%Y-%m-%dT%H:%M:%S") - outbox_path = File.join("public/outbox", date + ".json") - notes_path = File.join("public/notes", date + ".json") + date = Time.now.strftime('%Y-%m-%dT%H:%M:%S') + outbox_path = File.join('public/outbox', "#{date}.json") + notes_path = File.join('public/notes', "#{date}.json") - recipients = ["https://www.w3.org/ns/activitystreams#Public", params[:to]] - recipients += Dir[File.join("public/followers", "*.json")].collect { |f| JSON.parse(File.read(f))["actor"] } + recipients = ['https://www.w3.org/ns/activitystreams#Public', params[:to]] + recipients += Dir[File.join('public/followers', '*.json')].collect { |f| JSON.parse(File.read(f))['actor'] } recipients.delete ACTOR recipients.uniq! - + content = [] attachment = [] tag = [] extensions = { - :image => ["jpeg","png"], - :audio => ["flac","wav","mp3","ogg"], - :video => ["mp4","webm"] + image: %w[jpeg png], + audio: %w[flac wav mp3 ogg], + video: %w[mp4 webm] } params[:content].lines.each do |line| line.chomp! if line.match(/^http/) - ext = File.extname(line).sub('.','') - media_type = extensions.select{|k,v| v.include? ext}.keys[0].to_s + '/' + ext + ext = File.extname(line).sub('.', '') + media_type = extensions.select { |_k, v| v.include? ext }.keys[0].to_s + '/' + ext attachment << { 'type' => 'Document', 'mediaType' => media_type, @@ -31,7 +33,7 @@ post "/" do else tags = line.split(/\s+/).grep(/^#\w+$/) tags.each do |name| - href = File.join(SOCIAL_URL,'tags',name.sub('#','')) + href = File.join(SOCIAL_URL, 'tags', name.sub('#', '')) tag << { 'type' => 'Hashtag', 'href' => href, @@ -44,161 +46,129 @@ post "/" do end create = { - "@context" => "https://www.w3.org/ns/activitystreams", - "id" => File.join(SOCIAL_URL, outbox_path), - "type" => "Create", - "actor" => ACTOR, - "object" => { - "id" => File.join(SOCIAL_URL, notes_path), - "type" => "Note", - "attributedTo" => ACTOR, - "inReplyTo" => params[:inReplyTo], - "published" => date, - "content" => "

\n"+content.join("\n
")+"\n

", - "attachment" => attachment, - "tag" => tag, - "to" => recipients + '@context' => 'https://www.w3.org/ns/activitystreams', + 'id' => File.join(SOCIAL_URL, outbox_path), + 'type' => 'Create', + 'actor' => ACTOR, + 'object' => { + 'id' => File.join(SOCIAL_URL, notes_path), + 'type' => 'Note', + 'attributedTo' => ACTOR, + 'inReplyTo' => params[:inReplyTo], + 'published' => date, + 'content' => "

\n#{content.join("\n
")}\n

", + 'attachment' => attachment, + 'tag' => tag, + 'to' => recipients }, - "published" => date, - "to" => recipients + 'published' => date, + 'to' => recipients } - File.open(outbox_path, "w+") { |f| f.puts create.to_json } - File.open(notes_path, "w+") { |f| f.puts create["object"].to_json } + 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('#','')) + dir = File.join('public', 'tags', t['name'].sub('#', '')) FileUtils.mkdir_p dir - FileUtils.ln_s File.join('/srv/social/',notes_path), 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 } - send_signed create #, r } + # recipients.delete "https://www.w3.org/ns/activitystreams#Public" + # recipients.each { |r| send_signed create, r } + # send_signed create # , r } + outbox create redirect params['redirect'] end -post "/archive" do +post '/archive' do protected! - FileUtils.mv params['file'], "archive/" + FileUtils.mv params['file'], 'archive/' redirect to(params['redirect']) end -post "/delete" do # delete not supported by html forms +post '/delete' do # delete not supported by html forms protected! - params['file'] ? FileUtils.rm_f(params['file']) : FileUtils.rm_f(Dir["inbox/*.json"]) - params['redirect'] ? redirect(params['redirect']) : redirect('/') + FileUtils.rm_f(params['file'] || Dir['inbox/*.json']) + redirect(params['redirect'] || '/') end -post "/follow" do +post '/follow' do protected! actor, mention = parse_follow params['follow'] - follow = { "@context" => "https://www.w3.org/ns/activitystreams", - "id" => File.join(SOCIAL_URL, "following", mention+ ".json"), - "type" => "Follow", - "actor" => ACTOR, - "object" => actor, - 'to' => [ actor ] } - send_signed follow#, actor - redirect "/" + follow = { '@context' => 'https://www.w3.org/ns/activitystreams', + 'id' => File.join(SOCIAL_URL, 'following', "#{mention}.json"), + 'type' => 'Follow', + 'actor' => ACTOR, + 'object' => actor, + 'to' => [actor] } + send_signed follow # , actor + redirect '/' end -post "/unfollow" do +post '/unfollow' do protected! 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)), - 'to' => [ actor ] } - send_signed undo#, actor + following_path = File.join('public', 'following', "#{mention}.json") + if File.exist?(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)), + 'to' => [actor] } + send_signed undo # , actor FileUtils.rm following_path - redirect "/" + redirect '/' end end -post "/login" do - session["client"] = true if OpenSSL::Digest::SHA256.base64digest(params["secret"]) == File.read(".digest").chomp - redirect "/" +post '/login' do + session['client'] = (OpenSSL::Digest::SHA256.base64digest(params['secret']) == File.read('.digest').chomp) + redirect '/' +end + +get '/' do + redirect '/inbox' end -# private -["/", "/archive"].each do |path| - get path, :provides => 'html' do +['/inbox', '/archive', '/outbox'].each do |path| + get path, provides: 'html' do protected! - if path == '/' - @dir = 'inbox' - @alt_dir = '/archive' - @alt_name = 'archive' - else - @dir = path.sub('/','') - @alt_dir = '/' - @alt_name = 'inbox' - end + @items = fetch(File.join(SOCIAL_URL, path))['orderedItems'] threads - erb :index + erb :collection end end helpers do - def protected! halt 403 unless session['client'] end - def items - nr = 0 - 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']) - following_path = File.join('public', 'following', mention + '.json') - File.exists?(following_path) ? follow = 'unfollow' : follow = 'follow' - nr += 1 - { :id => item['id'], - :nr => nr, - :parent => item['inReplyTo'], - :file => file, - :actor_url => item['attributedTo'], - :mention => mention, - :follow => follow, - :content => item['content'], - :attachment => item['attachment'], - :indent => 0, - :replies => [] - } - end.compact - @items.last[:nr] = @items.last[:nr] - 2 unless @items.empty? + def outbox(activity) + curl("-X POST -d #{activity.to_json}", File.join(SOCIAL_URL, 'outbox')) end def threads - items @threads = [] @items.each do |i| - if i[:parent].nil? or @items.select{|it| it[:id] == i[:parent] }.empty? + i['indent'] = 0 + i['replies'] = [] + if i['inReplyTo'].nil? || @items.select { |it| it['id'] == i['inReplyTo'] }.empty? @threads << i else - @items.select{|it| it[:id] == i[:parent] }.each do |it| - i[:indent] = it[:indent] + 4 - it[:replies] << i + @items.select { |it| it['id'] == i['inReplyTo'] }.each do |it| + i['indent'] = it['indent'] + 4 + it['replies'] << i end end end end - def html item - @item = item - erb :item - end - - def parse_follow follow + def parse_follow(follow) case follow when /^#/ - actor = "https://relay.fedi.buzz/tag/#{follow.sub(/^#/,'')}" + actor = "https://relay.fedi.buzz/tag/#{follow.sub(/^#/, '')}" mention = follow when /^http/ actor = follow @@ -210,5 +180,4 @@ helpers do end [actor, mention] end - end diff --git a/helpers.rb b/helpers.rb index a51ea3d..fdbd894 100644 --- a/helpers.rb +++ b/helpers.rb @@ -1,78 +1,82 @@ -helpers do +# frozen_string_literal: true - def curl ext, url +require 'English' +helpers do + def curl(ext, url) + p url response = `/run/current-system/sw/bin/curl --fail-with-body -sSL #{ext} #{url}` - $?.success? ? response : nil + $CHILD_STATUS.success? ? response : nil end - def fetch url, accept = 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' + # def fetch(url, accept = 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"') + def fetch(url, accept = 'application/activity+json') response = curl("-H 'Accept: #{accept}'", url) response ? JSON.parse(response) : nil end # https://github.com/mastodon/mastodon/blob/main/app/lib/request.rb - def send_signed object#, url - + # , url + def send_signed(object) 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) - 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 + sha256 = OpenSSL::Digest.new('SHA256') + digest = "SHA-256=#{sha256.base64digest(body)}" + recipients = [object['to'], object['cc'], object['bto'], object['bcc'], object['audience']].flatten.compact.uniq + recipients.each do |url| + next if 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? - else - p "No inbox for #{url}" - end + 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 + ) + else + p "No inbox for #{url}" end end - end def people - File.read('cache/people.tsv').split("\n").collect {|l| l.chomp.split("\t")} + File.read('cache/people.tsv').split("\n").collect { |l| l.chomp.split("\t") } end - def mention actor - person = people.select{|p| p[1] == actor} + def mention(actor) + person = people.select { |p| p[1] == actor } if person.empty? a = fetch(actor) return nil unless a - mention = "#{a["preferredUsername"]}@#{URI(actor).host}" - File.open('cache/people.tsv','a'){|f| f.puts "#{mention}\t#{actor}"} + + mention = "#{a['preferredUsername']}@#{URI(actor).host}" + File.open('cache/people.tsv', 'a') { |f| f.puts "#{mention}\t#{actor}" } mention else person[0][0] end end - def actor mention + def actor(mention) mention = mention.sub(/^@/, '').chomp - actors = people.select{|p| p[0] == mention} + actors = people.select { |p| p[0] == mention } if actors.empty? - user, server = mention.split("@") - a = fetch("https://#{server}/.well-known/webfinger?resource=acct:#{mention}", "application/jrd+json") + _, server = mention.split('@') + a = fetch("https://#{server}/.well-known/webfinger?resource=acct:#{mention}", 'application/jrd+json') return nil unless a - actor = a["links"].select { |l| - l["rel"] == "self" - }[0]["href"] - File.open('cache/people.tsv','a'){|f| f.puts "#{mention}\t#{actor}"} + + actor = a['links'].select do |l| + l['rel'] == 'self' + end[0]['href'] + File.open('cache/people.tsv', 'a') { |f| f.puts "#{mention}\t#{actor}" } actor else actors[0][1] end end - end diff --git a/server.rb b/server.rb index 282c3e9..6379332 100644 --- a/server.rb +++ b/server.rb @@ -3,9 +3,12 @@ before '/inbox' do request.body.rewind # in case someone already read it @body = request.body.read - @activity = JSON.parse @body - @object = @activity['object'] - @object = fetch(@object) if @object.is_a?(String) && @object.match(/^http/) + unless @body.empty? + @activity = JSON.parse @body + @object = @activity['object'] + @object = fetch(@object) if @object.is_a?(String) && @object.match(/^http/) + halt 400 unless @object + end end # client-server @@ -17,6 +20,8 @@ end # server-server post '/inbox' do verify! + # file = File.join INBOX, "#{SecureRandom.uuid}.json" + # File.open(file, 'w+') { |f| f.puts @activity.to_json } type = @activity['type'].downcase.to_sym respond_to?(type) ? send(type) : p("Unknown activity: #{type}") end @@ -31,7 +36,16 @@ get '/.well-known/webfinger' do end end -['/outbox', '/following', '/followers'].each do |path| +get '/outbox' do + ordered_collection(OUTBOX).to_json +end + +get '/inbox' do + # protected! + ordered_collection(File.join(INBOX, 'note')).to_json +end + +['/following', '/followers'].each do |path| get path do ordered_collection(File.join(PUBLIC_DIR, path)).to_json end @@ -44,11 +58,12 @@ end helpers do # https://github.com/mastodon/mastodon/blob/main/app/controllers/concerns/signature_verification.rb def verify! - # verify digest + # digest sha256 = OpenSSL::Digest.new('SHA256') digest = "SHA-256=#{sha256.base64digest(@body)}" halt 403 unless digest == request.env['HTTP_DIGEST'] + # signature signature_params = {} request.env['HTTP_SIGNATURE'].split(',').each do |pair| k, v = pair.split('=') @@ -60,9 +75,8 @@ helpers do signature = Base64.decode64(signature_params['signature']) actor = fetch key_id - halt 200 if !actor && (@activity['type'] == 'Delete') # deleted users do not return actors - halt 403 unless actor + key = OpenSSL::PKey::RSA.new(actor['publicKey']['publicKeyPem']) comparison = headers.split(' ').map do |signed_params_name| @@ -79,10 +93,11 @@ helpers do end def create + return unless @object return if object_exists? File.open(object_file, 'w+') { |f| f.puts @object.to_json } - return unless @object && @object['inReplyTo'] + return unless @object['inReplyTo'] @object = fetch @object['inReplyTo'] create if @object @@ -125,13 +140,13 @@ helpers do 'actor' => ACTOR, 'object' => @activity, 'to' => [@activity['actor']] } - send_signed accept # , @activity['actor'] + send_signed accept end + # when "Like" # when "Move" # when "Add" # when "Remove" - # when "Like" # when "Block" def inbox diff --git a/views/collection.erb b/views/collection.erb new file mode 100644 index 0000000..4455b31 --- /dev/null +++ b/views/collection.erb @@ -0,0 +1,38 @@ + + + + + + +

<%= @dir %> +
+ +
+

+ <% @threads.each do |object| + @object = object %> + <%= erb :object %> + <% end %> +
+ +
+ + + diff --git a/views/index.erb b/views/index.erb deleted file mode 100644 index b512911..0000000 --- a/views/index.erb +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - -

<%= @dir %> -
- -
-

- <% @threads.each do |item| %> - <%= html item %> - <% end %> - <% if @dir == 'inbox' %> -
- -
- <% end %> - - - diff --git a/views/item.erb b/views/item.erb deleted file mode 100644 index 8036b39..0000000 --- a/views/item.erb +++ /dev/null @@ -1,54 +0,0 @@ -
- <%= @item[:mention] %>  - <% if @item[:mention] != ACCOUNT %> -
- - #<%= @item[:nr] %>' /> - -
-   -
- - #<%= @item[:nr] %>' /> - -
- <% if @dir == 'inbox' %> -   -
- - #<%= @item[:nr] %>' /> - -
- <% end %> - <% end %> - <%= @item[:content] %> - <% if @item[:attachment] - @item[:attachment].each do |att| - case att['mediaType'] - when /audio/ %> -
- <% when /image/ %> -
'>'> - <% when /video/ %> -
- <% else %> - <%= att %>
- '><%= att['url'] %> - <% end %> - <% end %> - <% end %> -

- -

-
-<% @item[:replies].each do |reply| %> - <%= html reply %> -<% end %> diff --git a/views/object.erb b/views/object.erb new file mode 100644 index 0000000..744a518 --- /dev/null +++ b/views/object.erb @@ -0,0 +1,57 @@ + +<% mention = mention @object['attributedTo'] + following_path = File.join(FOLLOWING, "#{mention}.json") + follow = File.exist?(following_path) ? 'unfollow' : 'follow' +%> +
em' id='<%= @object['id'] %>'> + ', target='_blank'><%= mention %>  +
+ ' /> + ' /> + +
+   +
+ ' /> + ' /> + +
+   +
+ ' /> + ' /> + +
+ <%= @object['content'] %> + <% if @object['attachment'] + @object['attachment'].each do |att| + case att['mediaType'] + when /audio/ %> +
+ <% when /image/ %> +
'>'> + <% when /video/ %> +
+ <% else %> + <%= att %>
+ '><%= att['url'] %> + <% end %> + <% end %> + <% end %> +

+ +

' style='display:none;' > + ' /> + ' /> + ' /> + +
+ + +
+
+<% @object['replies'].each do |reply| + @object = reply %> + <%= erb :object %> +<% end %> + -- cgit v1.2.3