diff options
-rw-r--r-- | activitypub.rb | 20 | ||||
-rw-r--r-- | client.rb | 20 | ||||
-rw-r--r-- | helpers.rb | 127 | ||||
-rw-r--r-- | public/follow.html | 10 | ||||
-rw-r--r-- | public/login.html | 29 | ||||
-rw-r--r-- | public/pdp8 | 43 | ||||
-rw-r--r-- | public/pdp8@social.pdp8.info.json (renamed from public/webfinger) | 0 | ||||
-rw-r--r-- | server.rb | 121 | ||||
-rw-r--r-- | views/collection.erb | 2 | ||||
-rw-r--r-- | views/object.erb | 2 |
10 files changed, 175 insertions, 199 deletions
diff --git a/activitypub.rb b/activitypub.rb index aef92a7..7416cca 100644 --- a/activitypub.rb +++ b/activitypub.rb @@ -6,26 +6,32 @@ require 'digest/sha2' require 'sinatra' SOCIAL_DIR = '/srv/social/' -INBOX_DIR = File.join(SOCIAL_DIR, 'inbox') PUBLIC_DIR = File.join(SOCIAL_DIR, 'public') +PRIVATE_DIR = File.join(SOCIAL_DIR, 'private') OUTBOX_DIR = File.join(PUBLIC_DIR, 'outbox') -FOLLOWERS = File.join(PUBLIC_DIR, 'followers') -FOLLOWING_DIR = File.join(PUBLIC_DIR, 'following') -TAGS = File.join(PUBLIC_DIR, 'tags') + +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') USER = 'pdp8' SOCIAL_DOMAIN = 'social.pdp8.info' MENTION = "#{USER}@#{SOCIAL_DOMAIN}" +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') -FOLLOWING_URL = File.join(SOCIAL_URL, 'following') + +CONTENT_TYPE = 'application/activity+json' +# CONTENT_TYPE = 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' enable :sessions set :session_secret, File.read('.secret').chomp -# set :default_content_type, 'application/activity+json' -set :default_content_type, 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' +set :default_content_type, CONTENT_TYPE set :port, 9292 require_relative 'helpers' @@ -64,7 +64,12 @@ end post '/delete' do protected! - selection(params).each { |f| FileUtils.rm f } + 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 + end redirect(params['anchor'] || '/inbox') end @@ -78,6 +83,8 @@ end post '/unfollow' do protected! actor, mention = parse_follow params['follow'] + outbox 'Undo', JSON.parse(File.read(following_path)), [actor] + p actor following_path = File.join(FOLLOWING_DIR, "#{mention}.json") if File.exist?(following_path) outbox 'Undo', JSON.parse(File.read(following_path)), [actor] @@ -100,17 +107,24 @@ end get path, provides: 'html' do protected! @dir = path.sub('/', '') - collection = ordered_collection(File.join(SOCIAL_DIR, path, 'note'))['orderedItems'] + # p(Kernel.const_get(@dir.upcase)) + # p(File.read(Kernel.const_get(@dir.upcase))) + collection = JSON.parse(File.read(Kernel.const_get(@dir.upcase)))['orderedItems'].uniq + # collection = ordered_collection(File.join(SOCIAL_DIR, path, 'note'))['orderedItems'] @threads = [] - collection.each_with_index do |object, _idx| + collection.each do |object| object['indent'] = 0 object['replies'] = [] if object['inReplyTo'].nil? || collection.select { |o| o['id'] == object['inReplyTo'] }.empty? @threads << object else collection.select { |o| o['id'] == object['inReplyTo'] }.each do |o| + next unless o['indent'] + object['indent'] = o['indent'] + 2 o['replies'] << object + # else + # p o end end end @@ -3,34 +3,6 @@ require 'English' helpers do - def curl(ext, url) - p url - response = `/run/current-system/sw/bin/curl --fail-with-body -sSL #{ext} #{url}` - if $CHILD_STATUS.success? - response - else - p response - nil - end - end - - def fetch(url, accept = 'application/activity+json') - uri = URI(url) - httpdate = Time.now.utc.httpdate - keypair = OpenSSL::PKey::RSA.new(File.read('private.pem')) - string = "(request-target): get #{uri.request_uri}\nhost: #{uri.host}\ndate: #{httpdate}" - 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\",signature=\"#{signature}\"" - - response = curl( - "-H 'Accept: #{accept}' -H 'Host: #{uri.host}' -H 'Date: #{httpdate}' -H 'Signature: #{signed_header}' ", url - ) - # response = curl("-H 'Accept: #{accept}'", url) - response ? JSON.parse(response) : nil - end - - # https://github.com/mastodon/mastodon/blob/main/app/lib/request.rb - # , url def outbox(type, object, recipients, add_recipients = false) activity = { '@context' => 'https://www.w3.org/ns/activitystreams', @@ -43,16 +15,34 @@ helpers do date = now.strftime('%Y-%m-%dT%H:%M:%S.%N') httpdate = now.utc.httpdate basename = "#{date}.json" - activity['id'] = File.join(OUTBOX_URL, basename) + 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 + + # save object if activity['object'] && activity['object']['type'] && !activity['object']['id'] - rel_path = File.join activity['object']['type'].downcase, basename - activity['object']['published'] = httpdate - activity['object']['id'] = File.join(OUTBOX_URL, rel_path) - File.open(File.join(OUTBOX_DIR, rel_path), 'w+') { |f| f.puts activity.to_json } + + 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 + FileUtils.mkdir_p File.dirname(object_path) + File.open(object_path, 'w+') { |f| f.puts object.to_json } end - File.open(File.join(OUTBOX_DIR, basename), 'w+') { |f| f.puts activity.to_json } + # 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 type == 'Follow' + # jj activity + # update_collection FOLLO, activity['id'] + # end + # 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') @@ -62,21 +52,17 @@ helpers do # put all recipients into 'to', avoid 'cc' 'bto' 'bcc' 'audience' !! activity['to'] = recipients if add_recipients inboxes = [] - # inboxes = if recipients.include? 'https://www.w3.org/ns/activitystreams#Public' - # people.collect { |p| p[2] }.uniq # cached sharedInboxes - # else - # [] - # end recipients.uniq.each do |url| next if [ACTOR, 'https://www.w3.org/ns/activitystreams#Public'].include? url - p 'FETCH', url actor = fetch url - p actor - next unless actor && actor['inbox'] + next unless actor - inbox = actor['endpoints']['sharedInbox'] - inboxes << (inbox || actor['inbox']) + 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) @@ -90,6 +76,55 @@ helpers do end end + def update_collection(path, objects, delete = false) + objects = [objects] unless objects.is_a? Array + File.open(path, 'r+') do |f| + f.flock(File::LOCK_EX) + json = f.read + collection = JSON.parse(json) + objects.each do |object| + if delete + collection['orderedItems'].delete_if { |o| o['id'] == object['id'] } + modified = true + else + ids = collection['orderedItems'].collect { |i| i['id'] } + collection['orderedItems'] << object unless ids.include?(object['id']) + modified = true + end + end + collection['totalItems'] = collection['orderedItems'].size + f.rewind + f.puts collection.to_json + f.truncate(f.pos) + end + end + + def fetch(url, accept = 'application/activity+json') + uri = URI(url) + httpdate = Time.now.utc.httpdate + keypair = OpenSSL::PKey::RSA.new(File.read('private.pem')) + string = "(request-target): get #{uri.request_uri}\nhost: #{uri.host}\ndate: #{httpdate}" + 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\",signature=\"#{signature}\"" + + response = curl( + "-H 'Accept: #{accept}' -H 'Host: #{uri.host}' -H 'Date: #{httpdate}' -H 'Signature: #{signed_header}' ", url + ) + # response = curl("-H 'Accept: #{accept}'", url) + response ? JSON.parse(response) : nil + end + + def curl(ext, url) + p url + response = `/run/current-system/sw/bin/curl --fail-with-body -sSL #{ext} #{url}` + if $CHILD_STATUS.success? + response + else + p response + nil + end + end + def mention(actor) person = people.select { |p| p[1] == actor } if person.empty? @@ -123,11 +158,11 @@ helpers do end def people - File.read('cache/people.tsv').split("\n").collect { |l| l.chomp.split("\t") } + File.read('private/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('cache/people.tsv', 'a') { |f| f.puts "#{mention}\t#{actor}\t#{sharedInbox}" } + File.open('private/people.tsv', 'a') { |f| f.puts "#{mention}\t#{actor}\t#{sharedInbox}" } end end diff --git a/public/follow.html b/public/follow.html index 7cbb37c..c1b8f8c 100644 --- a/public/follow.html +++ b/public/follow.html @@ -6,10 +6,12 @@ </head> <body> - <form action='/follow' method='post'> - <input name='follow' /> - <input type='submit' name='button' value='Follow' /> - </form> + <h1> + <form action='/follow' method='post'> + <input name='follow' /> + <input type='submit' name='button' value='Follow' /> + </form> + </h1> </body> </html>
\ No newline at end of file diff --git a/public/login.html b/public/login.html index 8fb479a..5026dfe 100644 --- a/public/login.html +++ b/public/login.html @@ -1,12 +1,17 @@ - <!DOCTYPE html> - <html lang='en'> - <head> - <link rel='stylesheet' type='text/css' href='/style.css'> - </head> - <body> - <form action='/login' method='post'> - <input type='password' name='secret' /> - <input type='submit' name='button' value='Login' /> - </form> - </body> - </html> +<!DOCTYPE html> +<html lang='en'> + +<head> + <link rel='stylesheet' type='text/css' href='/style.css'> +</head> + +<body> + <h1> + <form action='/login' method='post'> + <input type='password' name='secret' /> + <input type='submit' name='button' value='Login' /> + </form> + </h1> +</body> + +</html>
\ No newline at end of file diff --git a/public/pdp8 b/public/pdp8 deleted file mode 100644 index c066ea6..0000000 --- a/public/pdp8 +++ /dev/null @@ -1,43 +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", - "manuallyApprovesFollowers": false, - "icon": { - "type": "Image", - "url": "https://pdp8.info/pdp8.png" - }, - "attachment": [ - { - "type": "PropertyValue", - "name": "Web", - "value": "<a href=\"https://pdp8.info\">pdp8.info</a>" - }, - { - "type": "PropertyValue", - "name": "Fediverse", - "value": "<a rel=\"me\" href=\"https://social.pdp8.info/pdp8\">@pdp8@social.pdp8.info</a>" - }, - { - "type": "PropertyValue", - "name": "Matrix", - "value": "<a rel=\"me\" href=\"https://matrix.to/#/@pdp8:matrix.pdp8.info\">@pdp8:matrix.pdp8.info</a>" - } - ], - "endpoints": { - "sharedInbox": "https://social.pdp8.info/inbox" - }, - "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/webfinger b/public/pdp8@social.pdp8.info.json index 507b34e..507b34e 100644 --- a/public/webfinger +++ b/public/pdp8@social.pdp8.info.json @@ -14,43 +14,61 @@ post '/inbox' do type = @activity['type'].downcase.to_sym p type halt 501 unless respond_to?(type) - # jj @activity @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 + verify! unless type == :accept # pixelfed sends unsigned accept activities??? send(type) + halt 200 end # public get '/.well-known/webfinger' do - if request['resource'] == "acct:#{MENTION}" - send_file('./public/webfinger', - type: 'application/jrd+json') - else - halt 404 - end -end - -get '/outbox' do - ordered_collection(OUTBOX_DIR).to_json + halt 404 unless request['resource'] == "acct:#{MENTION}" + send_file(WEBFINGER, type: 'application/jrd+json') end -['/following', '/followers'].each do |path| +['/pdp8', '/following', '/followers', '/outbox', '/shared'].each do |path| get path do - ordered_collection(File.join(PUBLIC_DIR, path)).to_json + send_file(File.join(PUBLIC_DIR, path) + '.json', type: CONTENT_TYPE) end end -get '/pdp8' do - send_file('./public/pdp8') -end +helpers do + def create + return unless @object -get '/tags/:tag' do |tag| - ordered_collection(File.join(TAGS, tag)).to_json -end + 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 + return unless @object['type'] == 'Follow' + + update_collection FOLLOWING, @object['object'] + end + + def undo + return unless @object['type'] == 'Follow' + + update_collection FOLLOWERS, @object['actor'], true + end -helpers do # https://github.com/mastodon/mastodon/blob/main/app/controllers/concerns/signature_verification.rb def verify! # digest @@ -86,65 +104,4 @@ helpers do halt 403 unless key.verify(OpenSSL::Digest.new('SHA256'), signature, comparison) end - - def create - return unless @object - return if object_exists? - - File.open(object_file, 'w+') { |f| f.puts @object.to_json } - return unless @object['inReplyTo'] - - @object = fetch @object['inReplyTo'] - create if @object - end - - def announce - create - end - - def follow - File.open(File.join(FOLLOWERS, "#{mention(@activity['actor'])}.json"), 'w+') { |f| f.puts @body } - outbox 'Accept', @activity, [@activity['actor']] - end - - def accept - return unless @object['type'] == 'Follow' - - File.open(File.join(FOLLOWING_DIR, "#{mention(@object['object'])}.json"), 'w+') { |f| f.puts @object.to_json } - end - - def undo - return unless @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 - - def inbox - Dir[File.join(INBOX_DIR, 'note', '*.json')].collect do |file| - JSON.parse(File.read(file)) - end.sort_by { |o| o['published'] } - end - - def object_exists? - !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(dir, '*.json')].collect { |f| JSON.parse(File.read(f)) }.sort_by { |o| o['published'] } - { - '@context' => 'https://www.w3.org/ns/activitystreams', - 'summary' => "#{USER} #{dir}", - 'type' => 'OrderedCollection', - 'totalItems' => posts.size, - 'orderedItems' => posts - } - end end diff --git a/views/collection.erb b/views/collection.erb index 4cc449f..9640b7c 100644 --- a/views/collection.erb +++ b/views/collection.erb @@ -20,7 +20,7 @@ <% end %> <% unless @dir == 'shared' %> <form action='/delete' method='post'> - <input type='hidden' name='dir' value='/<%= @dir %>' /> + <input type='hidden' name='dir' value='<%= @dir %>' /> <input type='hidden' name='anchor' value='/<%= @dir %>' /> <button>Delete all</button> </form> diff --git a/views/object.erb b/views/object.erb index 3e6e617..6ac9f43 100644 --- a/views/object.erb +++ b/views/object.erb @@ -1,6 +1,6 @@ <% @idx +=1 mention = mention @object['attributedTo'] - follow = File.exist?(File.join(FOLLOWING_DIR, "#{mention}.json")) ? 'unfollow' : 'follow' + JSON.parse(File.read(FOLLOWING))['orderedItems'].include?(@object['attributedTo']) ? follow='unfollow' : follow='follow' %> <div style='margin-left:<%= @object['indent']%>em' id='<%= @idx %>'> <b><a href='<%= @object['attributedTo'] %>', target='_blank'><%= mention %></a></b> |