summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--activitypub.rb20
-rw-r--r--client.rb20
-rw-r--r--helpers.rb127
-rw-r--r--public/follow.html10
-rw-r--r--public/login.html29
-rw-r--r--public/pdp843
-rw-r--r--public/pdp8@social.pdp8.info.json (renamed from public/webfinger)0
-rw-r--r--server.rb121
-rw-r--r--views/collection.erb2
-rw-r--r--views/object.erb2
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'
diff --git a/client.rb b/client.rb
index c2c64ea..24d8795 100644
--- a/client.rb
+++ b/client.rb
@@ -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
diff --git a/helpers.rb b/helpers.rb
index 212c406..84f6a89 100644
--- a/helpers.rb
+++ b/helpers.rb
@@ -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
diff --git a/server.rb b/server.rb
index 3263d9d..1093566 100644
--- a/server.rb
+++ b/server.rb
@@ -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>&nbsp;