summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--activitypub.rb11
-rw-r--r--client.rb160
-rw-r--r--helpers.rb5
-rw-r--r--send.rb79
-rw-r--r--server.rb16
-rw-r--r--views/collection.erb47
-rw-r--r--views/object.erb71
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'
diff --git a/client.rb b/client.rb
index f90ca5f..8d34cf1 100644
--- a/client.rb
+++ b/client.rb
@@ -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
diff --git a/helpers.rb b/helpers.rb
index 0db6566..94ec2e1 100644
--- a/helpers.rb
+++ b/helpers.rb
@@ -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
diff --git a/send.rb b/send.rb
new file mode 100644
index 0000000..91d3e9b
--- /dev/null
+++ b/send.rb
@@ -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
diff --git a/server.rb b/server.rb
index 4fdf1e8..f337820 100644
--- a/server.rb
+++ b/server.rb
@@ -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>&nbsp;
- <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'] %>
- &nbsp;
- <em><%= @object['published'] %></em>
- &nbsp;
- <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' %>
- &nbsp;
- <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 %>
-