diff options
-rw-r--r-- | activitypub.rb | 11 | ||||
-rw-r--r-- | client.rb | 160 | ||||
-rw-r--r-- | helpers.rb | 5 | ||||
-rw-r--r-- | send.rb | 79 | ||||
-rw-r--r-- | server.rb | 16 | ||||
-rw-r--r-- | views/collection.erb | 47 | ||||
-rw-r--r-- | views/object.erb | 71 |
7 files changed, 135 insertions, 254 deletions
diff --git a/activitypub.rb b/activitypub.rb index f740406..90fe0c4 100644 --- a/activitypub.rb +++ b/activitypub.rb @@ -8,14 +8,9 @@ require 'sinatra' SOCIAL_DIR = '/srv/social/' PUBLIC_DIR = File.join(SOCIAL_DIR, 'public') PRIVATE_DIR = File.join(SOCIAL_DIR, 'private') -OUTBOX_DIR = File.join(PUBLIC_DIR, 'outbox') -TAGS_DIR = File.join(PUBLIC_DIR, 'tags') -OLD_INBOX = File.join(PRIVATE_DIR, 'inbox.json') FOLLOWERS = File.join(PUBLIC_DIR, 'followers.json') FOLLOWING = File.join(PUBLIC_DIR, 'following.json') -# OLD_OUTBOX = File.join(PUBLIC_DIR, 'outbox.json') -# SHARED = File.join(PUBLIC_DIR, 'shared.json') USER = 'pdp8' SOCIAL_DOMAIN = 'social.pdp8.info' @@ -24,10 +19,9 @@ WEBFINGER = File.join(PUBLIC_DIR, MENTION + '.json') SOCIAL_URL = "https://#{SOCIAL_DOMAIN}" ACTOR = File.join(SOCIAL_URL, USER) -# 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') } +TAGS = { dir: File.join(PUBLIC_DIR, 'tags'), url: File.join(SOCIAL_URL, 'tags') } CONTENT_TYPE = 'application/activity+json' # CONTENT_TYPE = 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' @@ -38,5 +32,6 @@ set :default_content_type, CONTENT_TYPE set :port, 9292 require_relative 'helpers' -require_relative 'client' require_relative 'server' +require_relative 'client' +require_relative 'send' @@ -1,162 +1,77 @@ # frozen_string_literal: true # client-server -post '/' do # TODO - protected! - - recipients = public - recipients << params[:to] - - content = [] - attachment = [] - tag = [] - extensions = { - 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 - attachment << { - 'type' => 'Document', - 'mediaType' => media_type, - 'url' => line - } - else - tags = line.split(/\s+/).grep(/^#\w+$/) - tags.each do |name| - # href = File.join(TAGS_URL, name.sub('#', '')) - tag_url = File.join(TAGS_URL, name.sub('#', '')) - tag << { - 'type' => 'Hashtag', - 'href' => tag_url, - 'name' => name - } - end - mentions = line.split(/\s+/).grep(/^@\w+@\S+$/) - mentions.each do |mention| - actor = actor(mention) - tag << { - 'type' => 'Mention', - 'href' => actor, - 'name' => mention - } +['/inbox', '/outbox'].each do |path| + get path do + protected! + box = path.sub('/', '') + collection = Dir[File.join(box, 'object', '*', '*.json')].collect { |f| JSON.parse(File.read(f)) } + threads = [] + collection.collect! do |object| + object.is_a?(String) && object.match(/^http/) ? fetch(object) : object + end + collection.compact! + collection.each do |object| + object['mention'] = mention object['attributedTo'] + if object['type'] == 'Audio' + audio_url = object['url'].select { |url| url['mediaType'].match('audio') }[0] + object['attachment'] = [{ 'url' => audio_url['href'], 'mediaType' => audio_url['mediaType'] }] end - content << line + object['replies'] = [] + threads << object if object['inReplyTo'].nil? || collection.select do |o| + o['id'] == object['inReplyTo'] + end.empty? end - end - - object = { - 'type' => 'Note', - 'attributedTo' => ACTOR, - 'inReplyTo' => params[:inReplyTo], - 'content' => "<p>\n#{content.join("\n<br>")}\n</p>", - 'attachment' => attachment, - 'tag' => tag, - 'to' => recipients - } - - activity = outbox 'Create', object, recipients, true - activity['object']['tag'].each do |tag| - next unless tag['type'] == 'Hashtag' - - tag_path = File.join(TAGS_DIR, tag['name'].sub('#', '')) + '.json' - unless File.exist? tag_path - File.open(tag_path, 'w+') do |f| - tag_collection = { - '@context' => 'https://www.w3.org/ns/activitystreams', - 'id' => tag['href'], - 'type' => 'OrderedCollection', - 'totalItems' => 0, - 'orderedItems' => [] - } - f.puts tag_collection.to_json + collection.each do |object| + collection.select { |o| o['id'] == object['inReplyTo'] }.each do |o| + o['replies'] << object end end - update_collection tag_path, activity['object']['id'] + threads.sort_by { |o| o['published'] }.to_json end - redirect(params['anchor'] || '/inbox') end post '/delete' do protected! - collection = Kernel.const_get(params['dir'].upcase) - if params['id'] - update_collection collection, params, true - else - update_collection collection, JSON.parse(File.read(collection))['orderedItems'], true + params['id'].each do |id| + file = find_file id + FileUtils.rm(file) if File.exist? file end - redirect(params['anchor'] || '/inbox') end post '/follow' do protected! actor, = parse_follow params['follow'] outbox 'Follow', actor, [actor] - redirect(params['anchor'] || '/inbox') end post '/unfollow' do protected! actor, mention = parse_follow params['follow'] - Dir[File.join(OUTBOX_DIR, 'follow', '*.json')].each do |f| + Dir[File.join(OUTBOX[:dir], 'follow', '*.json')].each do |f| activity = JSON.parse(File.read(f)) if activity['object'] == actor outbox 'Undo', activity, [actor] update_collection FOLLOWING, actor, true end end - redirect(params['anchor'] || '/inbox') 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 + src = find_file INBOX, params['id'] + object = JSON.parse(File.read(src)) recipients = public recipients << object['attributedTo'] - outbox 'Announce', params['id'], recipients - redirect(params['anchor'] || '/inbox') + # outbox 'Announce', object, recipients + dest = src.sub('/inbox/', '/outbox/') + FileUtils.mkdir_p File.dirname(dest) + FileUtils.mv src, dest end post '/login' do session['client'] = (OpenSSL::Digest::SHA256.base64digest(params['secret']) == File.read('.digest').chomp) - redirect '/inbox' -end - -get '/' do - protected! - redirect '/inbox' -end - -['/inbox', '/outbox'].each do |path| - get path, provides: 'html' do - protected! - @box = path.sub('/', '') - collection = Dir[File.join(@box, 'object', '*', '*.json')].collect { |f| JSON.parse(File.read(f)) } - @threads = [] - collection.collect! do |object| - object = fetch(object) if object.is_a?(String) && object.match(/^http/) - object - end - collection.each do |object| - object['replies'] = [] - @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| - o['replies'] << object - end - end - @threads.sort_by! { |t| t['published'] } - erb :collection - end end helpers do @@ -164,14 +79,15 @@ helpers do halt 403 unless session['client'] end - def selection(params) - selection = Dir[File.join(SOCIAL_DIR, params['dir'], '*', '*.json')] - params['id'] ? selection.select { |f| JSON.parse(File.read(f))['id'] == params['id'] } : selection + def find_file(id) + Dir[File.join('*', 'object', '*', '*.json')].select do |f| + JSON.parse(File.read(f))['id'] == id + end[0] end def public recipients = ['https://www.w3.org/ns/activitystreams#Public'] - recipients += Dir[File.join(FOLLOWERS, '*.json')].collect { |f| JSON.parse(File.read(f))['actor'] } + recipients += JSON.parse(File.read(FOLLOWERS))['orderedItems'] recipients.delete ACTOR recipients.uniq end @@ -24,6 +24,8 @@ helpers do def save_object(object, box) object = fetch(object) if object.is_a? String and object.match(/^http/) + return unless object + object['@context'] = 'https://www.w3.org/ns/activitystreams' basename = "#{object['published']}_#{mention(object['attributedTo'])}.json" object_rel_path = File.join 'object', object['type'].downcase, basename @@ -71,8 +73,7 @@ helpers do def curl(ext, url) p url - # p "/run/current-system/sw/bin/curl --fail-with-body -sSL #{ext} #{url}" - response = `/run/current-system/sw/bin/curl --fail-with-body -sSL #{ext} #{url}` + response = `/run/current-system/sw/bin/curl -H 'Content-Type: #{CONTENT_TYPE}' -H 'Accept: #{CONTENT_TYPE}' --fail-with-body -sSL #{ext} #{url}` if $CHILD_STATUS.success? response else @@ -0,0 +1,79 @@ +post '/' do # TODO + protected! + + recipients = public + recipients << params[:to] + + # content = [] + tag = [] + params[:content].lines.each do |line| + # line.chomp! + tags = line.split(/\s+/).grep(/^#\w+$/) + tags.each do |name| + tag_url = File.join(TAGS[:url], name.sub('#', '')) + tag << { + 'type' => 'Hashtag', + 'href' => tag_url, + 'name' => name + } + end + + mentions = line.split(/\s+/).grep(/^@\w+@\S+$/) + mentions.each do |mention| + actor = actor(mention) + tag << { + 'type' => 'Mention', + 'href' => actor, + 'name' => mention + } + end + # content << line + end + + attachment = [] + extensions = { + image: %w[jpeg png], + audio: %w[flac wav mp3 ogg], + video: %w[mp4 webm] + } + params[:media].each do |_media| + 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, + 'url' => line + } + end + + object = { + 'type' => 'Note', + 'attributedTo' => ACTOR, + 'inReplyTo' => params[:inReplyTo], + 'content' => "<p>\n#{content.join("\n<br>")}\n</p>", + 'attachment' => attachment, + 'tag' => tag, + 'to' => recipients + } + + activity = outbox 'Create', object, recipients, true + activity['object']['tag'].each do |tag| + next unless tag['type'] == 'Hashtag' + + tag_path = File.join(TAGS_DIR, tag['name'].sub('#', '')) + '.json' + next if File.exist? tag_path + + File.open(tag_path, 'w+') do |f| + tag_collection = { + '@context' => 'https://www.w3.org/ns/activitystreams', + 'id' => tag['href'], + 'type' => 'OrderedCollection', + 'totalItems' => 0, + 'orderedItems' => [] + } + f.puts tag_collection.to_json + end + # update_collection tag_path, activity['object']['id'] + end + redirect(params['anchor'] || '/inbox') +end @@ -12,7 +12,7 @@ post '/inbox' do 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??? + # verify! # pixelfed sends unsigned activities??? save_activity(@activity, INBOX) type = @activity['type'].downcase.to_sym send(type) if %i[create announce follow accept undo].include? type @@ -20,6 +20,14 @@ post '/inbox' do end # public +get '/' do + redirect 'https://pdp8.info' +end + +get '/pdp8', provides: 'html' do + redirect 'https://pdp8.info' +end + get '/.well-known/webfinger' do halt 404 unless request['resource'] == "acct:#{MENTION}" send_file(WEBFINGER, type: 'application/jrd+json') @@ -39,7 +47,7 @@ helpers do def create @object ||= @activity['object'] @object = save_object @object, INBOX - return unless @object['inReplyTo'] + return unless @object and @object['inReplyTo'] @object = @object['inReplyTo'] create @@ -133,12 +141,12 @@ helpers do inboxes.compact.uniq.each do |inbox| uri = URI(inbox) httpdate = Time.now.utc.httpdate - string = "(request-target): post #{uri.request_uri}\nhost: #{uri.host}\ndate: #{httpdate}\ndigest: #{digest}\ncontent-type: application/activity+json" + string = "(request-target): post #{uri.request_uri}\nhost: #{uri.host}\ndate: #{httpdate}\ndigest: #{digest}\ncontent-type: #{CONTENT_TYPE}" 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 + "-X POST -H 'Host: #{uri.host}' -H 'Date: #{httpdate}' -H 'Digest: #{digest}' -H 'Signature: #{signed_header}' --data-binary '#{body}'", inbox ) end activity diff --git a/views/collection.erb b/views/collection.erb deleted file mode 100644 index c169e0b..0000000 --- a/views/collection.erb +++ /dev/null @@ -1,47 +0,0 @@ -<!DOCTYPE html> -<html lang='en'> - <head> - <link rel='stylesheet' type='text/css' href='/style.css'> - </head> - <body> - <h1><%= @box %> - <% dirs = ['inbox','outbox'] - dirs.delete(@box) - dirs.each do |d| %> - <form action='/<%= d %>' method='get'> - <button><%= d %></button> - </form> - <% end %> - </h1> - <% @idx = 0 - @threads.each do |object| - @object = object %> - <%= erb :object %> - <% end %> - <% unless @box == 'shared' %> - <form action='/delete' method='post'> - <input type='hidden' name='dir' value='<%= @box %>' /> - <input type='hidden' name='anchor' value='/<%= @box %>' /> - <button>Delete all</button> - </form> - <% end %> - </body> - <script> - const reply_buttons = document.querySelectorAll(".reply"); - for (const button of reply_buttons) { - button.addEventListener('click', function() { - const form = document.getElementById('form' + button.getAttribute('data-index')); - button.style.display = 'none'; - form.style.display = 'block'; - }); - }; - const cancel_buttons = document.querySelectorAll(".cancel"); - for (const button of cancel_buttons) { - button.addEventListener('click', function() { - const form = document.getElementById('form' + button.getAttribute('data-index')); - button.style.display = 'block'; - form.style.display = 'none'; - }); - }; - </script> -</html> diff --git a/views/object.erb b/views/object.erb deleted file mode 100644 index b4b7d89..0000000 --- a/views/object.erb +++ /dev/null @@ -1,71 +0,0 @@ -<% @idx +=1 - mention = mention @object['attributedTo'] - JSON.parse(File.read(FOLLOWING))['orderedItems'].include?(@object['attributedTo']) ? follow='unfollow' : follow='follow' - @indent = 0 unless @object['inReplyTo'] -%> -<div style='margin-left:<%= @indent%>em' id='<%= @idx %>'> - <b><a href='<%= @object['attributedTo'] %>', target='_blank'><%= mention %></a></b> - <form action='/<%= follow %>' method='post'> - <input type='hidden' name='follow' value='<%= @object['attributedTo'] %>' /> - <input type='hidden' name='anchor' value='/<%= @box %>#<%= @idx %>' /> - <button><%= follow.capitalize %></button> - </form> - <% unless @object['inReplyTo'] %> - - <em><%= @object['published'] %></em> - - <form action='/delete' method='post'> - <input type='hidden' name='id' value='<%= @object['id'] %>' /> - <input type='hidden' name='dir' value='<%= @box %>' /> - <input type='hidden' name='anchor' value='/<%= @box %>#<%= @idx %>' /> - <button>Delete</button> - </form> - <% end %> - <% unless @box == 'shared' %> - - <form action='/share' method='post'> - <input type='hidden' name='id' value='<%= @object['id'] %>' /> - <input type='hidden' name='dir' value='<%= @box %>' /> - <input type='hidden' name='anchor' value='/<%= @box %>#<%= @idx %>' /> - <button>Share</button> - </form> - <% end %> - <% unless @object['content'].match(/^<p>/) %> - <p> - <% end %> - <%= @object['content'] %> - <% if @object['attachment'] - @object['attachment'].each do |att| - case att['mediaType'] - when /audio/ %> - <br><audio controls=''><source src='<%= att['url'] %>' type='<%= att['mediaType'] %>'></audio> - <% when /image/ %> - <br><a href='<%= att['url'] %>'><img src='<%= att['url'] %>'></a> - <% when /video/ %> - <br><video controls=''><source src='<%= att['url'] %>' type='<%= att['mediaType'] %>'></video> - <% else %> - <%= att %><br> - <a href='<%= att['url'] %>'><%= att['url'] %></a> - <% end %> - <% end %> - <% end %> - <p> - <% unless @box == 'shared' %> - <button class='reply' data-index='<%= @idx %>'>Reply</button> - <form action='/' method='post' id='form<%= @idx %>' style='display:none;' > - <input type='hidden' name='to' value='<%= @object['attributedTo'] %>' /> - <input type='hidden' name='inReplyTo' value='<%= @object['id'] %>' /> - <input type='hidden' name='anchor' value='/<%= @box %>#<%= @idx %>' /> - <textarea name='content'></textarea> - <br> - <button class='cancel' data-index='<%= @idx %>'>Cancel</button> - <input type='submit' value='Send'> - </form> - <% end %> -</div> -<% @object['replies'].each do |reply| - @indent += 2 - @object = reply %> - <%= erb :object %> -<% end %> - |