diff options
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | application.rb | 150 | ||||
-rwxr-xr-x | client.rb | 72 |
3 files changed, 185 insertions, 39 deletions
@@ -1 +1,3 @@ *.pem +.usr +.pwd diff --git a/application.rb b/application.rb index 029b04e..9e4e3a8 100644 --- a/application.rb +++ b/application.rb @@ -1,3 +1,11 @@ +# TODO +# run as service +# federation +# client post media +# client follow +# client get media +# server follow +# test with pleroma etc require 'json' require 'net/http' require 'uri' @@ -5,6 +13,7 @@ require 'base64' require 'securerandom' require 'fileutils' require 'digest/sha2' +require 'nokogiri' USER = "pdp8" WWW_DOMAIN = "pdp8.info" @@ -15,13 +24,11 @@ ACCOUNT = "#{USER}@#{SOCIAL_DOMAIN}" SOCIAL_URL = "https://#{SOCIAL_DOMAIN}" ACTOR = File.join(SOCIAL_URL, USER) -MATRIX = "@#{USER}:matrix.#{WWW_DOMAIN}" - class Application def call(env) code = 404 type = 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' - response = "not allowed" + response = "Not found." case env['REQUEST_METHOD'] @@ -40,6 +47,7 @@ class Application puts input when "Follow" File.open(File.join("followers", SecureRandom.uuid + ".json"), "w+") { |f| f.puts input } + # TODO return accept activity when "Undo" puts input else @@ -52,33 +60,23 @@ class Application response = "Request body contains invalid json." end else - code = 401 - response = "Verification failed for POST to #{env["REQUEST_URI"]}." + code = 403 + response = "Key verification failed for POST to #{env["REQUEST_URI"]}." end when "/outbox" # receive from client - # TODO auth if auth(env) - input = JSON.parse(input) - input["type"] == "Create" ? activity = input : activity = activity(input) # expand object to create activity - add_id activity - save activity - FileUtils.ln_s File.join('..', path(activity)), "outbox" - code, response = send activity, ["to", "bto", "cc", "bcc", "audience"].collect { |d| - activity[d] - }.flatten.uniq.compact - code = 200 - response = "OK" + code, response = process input else code = 403 - response = "forbidden" + response = "You are not allowed to POST to #{env["REQUEST_URI"]}." end end when 'GET' - case env["REQUEST_PATH"] + case env["REQUEST_URI"] # REQUEST_PATH does not contain queries when "/.well-known/webfinger?resource=acct:#{ACCOUNT}" type = "application/jrd+json" @@ -90,8 +88,17 @@ class Application response = File.read(USER) code = 200 - when %r{/[inbox|outbox|following|followers|likes|shares]} - response = ordered_collection env["REQUEST_PATH"] + when "/inbox" + if auth(env) + type, response = format ordered_collection(env["REQUEST_PATH"]), env["HTTP_ACCEPT"] + code = 200 + else + code = 403 + response = "You are not allowed to GET #{env["REQUEST_URI"]}." + end + + when %r{/[outbox|following|followers|likes|shares]} + response = ordered_collection(env["REQUEST_PATH"]).to_json code = 200 end @@ -99,25 +106,83 @@ class Application [code, { "Content-Type" => type }, [response]] end - def add_id object - object["id"] = File.join(SOCIAL_URL, object["type"].downcase, SecureRandom.uuid + ".json") - end - - def activity object + def process input date = Time.now.strftime("%Y-%m-%dT%H:%M:%S") - object["attributedTo"] = ACTOR - object["published"] = date - add_id object - save object - { + # TODO media attachments, hashtags + note = { "@context" => "https://www.w3.org/ns/activitystreams", + "id" => File.join(SOCIAL_URL, "note", SecureRandom.uuid + ".json"), + "type" => "Note", + "attributedTo" => ACTOR, + "published" => date, + "content" => "" + } + create = { + "@context" => "https://www.w3.org/ns/activitystreams", + "id" => File.join(SOCIAL_URL, "create", SecureRandom.uuid + ".json"), "type" => "Create", "actor" => ACTOR, - "object" => object, + "object" => note, "published" => date, - "to" => object["to"], - "cc" => object["cc"] } + recipients = [] + if /^@/.match input + mentions, input = input.split("\n", 2) + mentions.split(/, */).each do |m| + recipients << actor(m.chomp) + end + end + note["content"] = input.lines.select { |l| !l.empty? }.join("<br>") + recipients += Dir[File.join("followers", "*.json")].collect { |f| JSON.parse(File.read(f))["actor"] } + recipients.delete ACTOR + recipients.uniq! + note["to"] = recipients + create["to"] = recipients + + save create + save note + FileUtils.ln_s File.join('..', path(create)), "outbox" + + responses = send create, recipients + if responses.collect { |r| r.code.to_i }.uniq.max < 400 + code = 200 + response = "OK" + else + code = 502 + response = responses.select { |r| r.code.to_i >= 400 }.collect { |r| r.body }.uniq + end + [code, response] + end + + def format response, accept + if accept == "text/plain" + response = response["orderedItems"].collect.with_index do |r, i| + object = r["object"] + doc = Nokogiri::HTML(object["content"]) + str = "#{i}\t#{object["published"]}\t#{}\n#{doc.text}" + str << "\n" + object["attachment"].collect { |att| + `kitty +kitten icat #{att["url"]}` + }.join("\n") if object["attachment"] + str + end.join("\n\n") + type = "text/plain" + else + response = response.to_json + type = 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' + end + [type, response] + end + + def actor account + account = account.sub(/^@/, '').chomp + user, server = account.split("@") + header = { 'Accept' => "application/jrd+json" } + uri = URI("https://" + server + "/.well-known/webfinger?resource=acct:#{account}") + http = Net::HTTP.new(uri.host, uri.port) + http.use_ssl = true + request = Net::HTTP::Get.new(uri.request_uri, header) + response = http.request(request) + JSON.parse(response.body)["links"].select { |l| l["rel"] == "self" }[0]["href"] end def path object @@ -131,8 +196,10 @@ class Application end def send object, urls + puts object, urls # https://github.com/mastodon/mastodon/blob/main/app/lib/request.rb keypair = OpenSSL::PKey::RSA.new(File.read('private.pem')) + responses = [] urls.each do |url| date = Time.now.utc.httpdate uri = URI.parse(url) @@ -158,22 +225,24 @@ class Application request = Net::HTTP::Post.new(uri.request_uri, header) request.body = body - response = http.request(request) - # TODO return error if response.code > 400 - puts(response.body, response.code) + responses << http.request(request) end + puts responses + responses end def ordered_collection dir collection = dir.sub(/^\//, "") - posts = Dir[File.join(collection, "*.json")].sort.reverse.collect { |f| p f; JSON.parse(File.read f) } + posts = Dir[File.join(collection, "*.json")].collect { |f| + p f; JSON.parse(File.read f) + }.sort_by { |o| o["published"] } { "@context" => "https://www.w3.org/ns/activitystreams", "summary" => "#{USER} #{collection}", "type" => "OrderedCollection", "totalItems" => posts.size, "orderedItems" => posts, - }.to_json + } end def verify env @@ -220,6 +289,9 @@ class Application end def auth env - true + auth = Rack::Auth::Basic::Request.new(env) + usr = File.read(".usr").chomp + pwd = File.read(".pwd").chomp + auth.provided? && auth.basic? && auth.credentials && auth.credentials == [usr, pwd] end end diff --git a/client.rb b/client.rb new file mode 100755 index 0000000..0ef2896 --- /dev/null +++ b/client.rb @@ -0,0 +1,72 @@ +#!/usr/bin/env ruby +# TODO +# run as service +# client post md +# direct from client (key) +# via server (auth) +require 'json' +require 'net/http' +require 'uri' +require 'base64' +require 'securerandom' +require 'fileutils' +require 'digest/sha2' + +USER = "pdp8" +WWW_DOMAIN = "pdp8.info" +WWW_URL = "https://#{WWW_DOMAIN}" +SOCIAL_DOMAIN = "social.#{WWW_DOMAIN}" + +ACCOUNT = "#{USER}@#{SOCIAL_DOMAIN}" +SOCIAL_URL = "https://#{SOCIAL_DOMAIN}" +ACTOR = File.join(SOCIAL_URL, USER) + +=begin +MATRIX = "@#{USER}:matrix.#{WWW_DOMAIN}" + +post = { + "@context" => "https://www.w3.org/ns/activitystreams", + "type" => "Note", + "content" => "" +} + +def webfinger account + account = account.sub(/^@/, '').chomp + user, server = account.split("@") + header = { 'Accept' => "application/jrd+json" } + uri = URI("https://" + server + "/.well-known/webfinger?resource=acct:#{account}") + http = Net::HTTP.new(uri.host, uri.port) + http.use_ssl = true + request = Net::HTTP::Get.new(uri.request_uri, header) + response = http.request(request) + JSON.parse(response.body)["links"].select { |l| l["rel"] == "self" }[0]["href"] +end + +ARGF.each do |line| + if /^(To|Cc|Bcc):/.match line + dest, addresses = line.split(/: */) + dest = dest.downcase + post[dest] ||= [] + addresses.split(/, */).each do |add| + post[dest] << webfinger(add.chomp) + end + else + post["content"] << line + end +end +=end + +uri = URI.parse(File.join SOCIAL_URL, "outbox") +http = Net::HTTP.new(uri.host, uri.port) +http.use_ssl = true +header = { 'Content-Type' => 'text/plain' } +# header = { 'Content-Type' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' } +request = Net::HTTP::Post.new(uri.request_uri, header) +usr = File.read(".usr").chomp +pwd = File.read(".pwd").chomp +request.basic_auth(usr, pwd) +request.body = File.read ARGV[0] + +response = http.request(request) +# TODO return error if response.code > 400 +puts(response.body, response.code) |