From d2c4d3b49e6b790d14f6dcb94c2ae0641559d2cf Mon Sep 17 00:00:00 2001 From: pdp8 Date: Fri, 19 May 2023 22:50:54 +0200 Subject: follow and followers --- application.rb | 133 ++++++++++++++++++++++++++++++++++++++++----------------- client.rb | 72 ------------------------------- post.rb | 45 ------------------- social | 33 ++++++++++++++ 4 files changed, 126 insertions(+), 157 deletions(-) delete mode 100755 client.rb delete mode 100755 post.rb create mode 100755 social diff --git a/application.rb b/application.rb index 9e4e3a8..66b47b2 100644 --- a/application.rb +++ b/application.rb @@ -47,9 +47,27 @@ class Application puts input when "Follow" File.open(File.join("followers", SecureRandom.uuid + ".json"), "w+") { |f| f.puts input } - # TODO return accept activity + accept = { "@context" => "https://www.w3.org/ns/activitystreams", + "id" => File.join(SOCIAL_URL + "#accepts", SecureRandom.uuid), + "type" => "Accept", + "actor" => ACTOR, + "object" => JSON.parse(input) } + send accept, [accept["object"]["actor"]] when "Undo" - puts input + o = object["object"] + # puts o + case o["type"] + when "Follow" + Dir["followers/*.json"].each do |follower| + puts follower + # puts JSON.parse(File.read(follower))["actor"] + if JSON.parse(File.read(follower))["actor"] == o["actor"] + FileUtils.rm follower + end + end + # ordered_collection("followers") + end + # puts input else puts input end @@ -66,12 +84,50 @@ class Application when "/outbox" # receive from client if auth(env) - code, response = process input + code, response = parse input + else + code = 403 + response = "You are not allowed to POST to #{env["REQUEST_URI"]}." + end + + when "/follow" # receive from client + if auth(env) + input.split.each do |mention| + actor = actor(mention) + follow = { "@context" => "https://www.w3.org/ns/activitystreams", + "id" => File.join(SOCIAL_URL, "following", SecureRandom.uuid + ".json"), + "type" => "Follow", + "actor" => ACTOR, + "object" => actor } + save follow + puts(send follow, [actor]) + code = 200 + response = "OK" + end else code = 403 response = "You are not allowed to POST to #{env["REQUEST_URI"]}." end + when "/unfollow" # receive from client + if auth(env) + input.split.each do |mention| + actor = actor(mention) + Dir["following/*.json"].each do |f| + follow = JSON.parse(File.read(f)) + puts follow + if follow["object"] == actor + undo = { "@context" => "https://www.w3.org/ns/activitystreams", + "id" => File.join(SOCIAL_URL + "#undo", SecureRandom.uuid), + "type" => "Undo", + "actor" => ACTOR, + "object" => follow } + send undo, [actor] + FileUtils.rm f + end + end + end + end end when 'GET' @@ -106,7 +162,7 @@ class Application [code, { "Content-Type" => type }, [response]] end - def process input + def parse input date = Time.now.strftime("%Y-%m-%dT%H:%M:%S") # TODO media attachments, hashtags note = { @@ -173,30 +229,17 @@ class Application [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 - object["id"].sub(SOCIAL_URL, '').sub('/', '') - end - - def save object - path = path object - FileUtils.mkdir_p File.dirname(path) - File.open(path, "w+") { |f| f.puts object.to_json } + def actor mention + mention = mention.sub(/^@/, '').chomp + user, server = mention.split("@") + get("https://#{server}/.well-known/webfinger?resource=acct:#{mention}", + "application/jrd+json")["links"].select { |l| + l["rel"] == "self" + }[0]["href"] end def send object, urls - puts 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 = [] @@ -227,7 +270,7 @@ class Application responses << http.request(request) end - puts responses + # puts responses responses end @@ -245,6 +288,30 @@ class Application } end + def inbox uri + URI(get(uri)["inbox"]).request_uri + end + + def get url, accept = 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' + uri = URI(url) + http = Net::HTTP.new(uri.host, uri.port) + http.use_ssl = true + header = { 'Accept' => accept } + request = Net::HTTP::Get.new(uri.request_uri, header) + response = http.request(request) + JSON.parse(response.body) + end + + def path object + object["id"].sub(SOCIAL_URL, '').sub('/', '') + end + + def save object + path = path object + FileUtils.mkdir_p File.dirname(path) + File.open(path, "w+") { |f| f.puts object.to_json } + end + def verify env # https://github.com/mastodon/mastodon/blob/main/app/controllers/concerns/signature_verification.rb # TODO verify digest @@ -274,20 +341,6 @@ class Application key.verify(OpenSSL::Digest.new('SHA256'), signature, comparison) end - def get url - uri = URI(url) - http = Net::HTTP.new(uri.host, uri.port) - http.use_ssl = true - header = { 'Accept' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' } - request = Net::HTTP::Get.new(uri.request_uri, header) - response = http.request(request) - JSON.parse(response.body) - end - - def inbox uri - URI(get(uri)["inbox"]).request_uri - end - def auth env auth = Rack::Auth::Basic::Request.new(env) usr = File.read(".usr").chomp diff --git a/client.rb b/client.rb deleted file mode 100755 index 0ef2896..0000000 --- a/client.rb +++ /dev/null @@ -1,72 +0,0 @@ -#!/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) diff --git a/post.rb b/post.rb deleted file mode 100755 index c5e7cd7..0000000 --- a/post.rb +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env ruby -require 'json' -require 'time' -require 'openssl' -require 'base64' -require 'net/http' -require 'uri' - -# document = { "a" => 2 } # .to_json -document = { - "@context": "https://www.w3.org/ns/activitystreams", - "type": "Like", - "actor": "https://example.net/~mallory", - "to": ["https://hatchat.example/sarah/", - "https://example.com/peeps/john/"], - "object": { - "@context": { "@language": "en" }, - "id": "https://example.org/~alice/note/23", - "type": "Note", - "attributedTo": "https://example.org/~alice", - "content": "I'm a goat" - } -} -date = Time.now.utc.httpdate -keypair = OpenSSL::PKey::RSA.new(File.read('private.pem')) -signed_string = "(request-target): post /inbox\nhost: social.pdp8.info\ndate: #{date}" -signed_string = keypair.sign(OpenSSL::Digest::SHA256.new, signed_string) -signature = Base64.urlsafe_encode64(signed_string).encode("UTF-8") -signed_header = 'keyId="https://social.pdp8.info/pdp8",headers="(request-target) host date",signature="' + signature + '"' - -uri = URI.parse("https://social.pdp8.info/inbox") -http = Net::HTTP.new(uri.host, uri.port) -http.use_ssl = true -http.verify_mode = OpenSSL::SSL::VERIFY_NONE -header = { - 'Accept' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', - 'Host' => 'social.pdp8.info', - 'Date' => date, - 'Signature' => signed_header, -} -request = Net::HTTP::Post.new(uri.request_uri, header) -request.body = document.to_json - -response = http.request(request) -puts(response.body, response.code) diff --git a/social b/social new file mode 100755 index 0000000..2a57153 --- /dev/null +++ b/social @@ -0,0 +1,33 @@ +#!/usr/bin/env ruby +require 'net/http' +require 'uri' + +USER = "pdp8" +WWW_DOMAIN = "pdp8.info" +SOCIAL_DOMAIN = "social.#{WWW_DOMAIN}" +SOCIAL_URL = "https://#{SOCIAL_DOMAIN}" + +def post path, body + uri = URI.parse(File.join SOCIAL_URL, path) + http = Net::HTTP.new(uri.host, uri.port) + http.use_ssl = true + header = { 'Content-Type' => 'text/plain' } + 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 = body + response = http.request(request) + # TODO return error if response.code > 400 + puts(response.body, response.code) +end + +# cmd = ARGV.shift +case ARGV.shift +when "post" + post "outbox", File.read(ARGV[0]) +when "follow" + post "follow", ARGV.join(" ") +when "unfollow" + post "unfollow", ARGV.join(" ") +end -- cgit v1.2.3