summaryrefslogtreecommitdiff
path: root/application.rb
diff options
context:
space:
mode:
Diffstat (limited to 'application.rb')
-rw-r--r--application.rb415
1 files changed, 0 insertions, 415 deletions
diff --git a/application.rb b/application.rb
deleted file mode 100644
index c8a657c..0000000
--- a/application.rb
+++ /dev/null
@@ -1,415 +0,0 @@
-# TODO
-# unwrap and save object from create
-# boost
-# archive
-# threads
-# federation
-# client post media
-# test with pleroma etc
-require 'json'
-require 'net/http'
-require 'uri'
-require 'base64'
-require 'securerandom'
-require 'fileutils'
-require 'digest/sha2'
-require 'nokogiri'
-
-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)
-
-class Application
- def call(env)
- code = 404
- type = 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'
- response = "Not found."
-
- case env['REQUEST_METHOD']
-
- when 'POST'
- input = env["rack.input"].read
- case env["REQUEST_PATH"]
-
- when "/inbox" # receive from server
- if verify(env)
- begin
- object = JSON.parse(input)
- case object["type"]
- when "Create"
- File.open(File.join("inbox", SecureRandom.uuid + ".json"), "w+") { |f| f.puts input }
- # File.open(File.join("inbox", input["published"] + ".json"), "w+") { |f| f.puts input["object"] }
- when "Delete"
- puts input
- when "Follow"
- File.open(File.join("followers", SecureRandom.uuid + ".json"), "w+") { |f| f.puts input }
- 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"
- o = object["object"]
- case o["type"]
- when "Follow"
- Dir["followers/*.json"].each do |follower|
- if JSON.parse(File.read(follower))["actor"] == o["actor"]
- FileUtils.rm follower
- end
- end
- else
- puts input
- end
- else
- puts input
- end
- code = 200
- response = "OK"
- rescue => e
- puts input, e.to_s
- response = "Request body contains invalid json."
- end
- else
- code = 403
- response = "Key verification failed for POST to #{env["REQUEST_URI"]}."
- end
-
- when %r{/delete} # receive from client
- if auth(env)
- FileUtils.rm env["REQUEST_URI"].sub("/delete/", "")
- return [302, { "Location" => "/inbox" }, []]
- end
-
- when "/outbox" # receive from client
- if auth(env)
- 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'
-
- case env["REQUEST_URI"] # REQUEST_PATH does not contain queries
-
- when "/.well-known/webfinger?resource=acct:#{ACCOUNT}"
- type = "application/jrd+json"
- response = File.read("webfinger")
- code = 200
-
- when "/#{USER}"
- # TODO serve html
- response = File.read(USER)
- code = 200
-
- when "/inbox"
- if auth(env)
- case env["HTTP_ACCEPT"]
- when /json/
- response = ordered_collection(env["REQUEST_PATH"]).to_json
- else
- type = "text/html"
- response = html env["REQUEST_PATH"]
- end
- 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
-
- end
- [code, { "Content-Type" => type }, [response]]
- end
-
- def html path
- html = "<!DOCTYPE html>\n<html lang='en'>\n\t<body>"
- Dir[File.join(path.sub(/^\//, ''), "*")].sort_by { |f| File.stat(f).ctime }.each do |file|
- item = JSON.parse(File.read(file))
- html << "\n\t\t<b>#{mention item["actor"]}</b>&nbsp;<i>#{item["object"]["published"].sub("T",
- " ")}</i><p>#{item["object"]["content"]}"
- if item["object"]["attachment"]
- item["object"]["attachment"].each do |att|
- case att["mediaType"]
- when /audio/
- html << "\n<br><audio controls=''><source src='#{att["url"]}' type='#{att["mediaType"]}'></audio>"
- when /image/
- html << "\n<br><a href='#{att["url"]}'><img src='#{att["url"]}'></a>"
- when /video/
- html << "\n<br><video controls=''><source src='#{att["url"]}' type='#{att["mediaType"]}'></video>"
- else
- html << att + "<br>"
- html << "\n<a href='#{att["url"]}'>#{att["url"]}</a>"
- end
- end
- end
- html << "<p>
- <form action='#{File.join "delete", file}' method='post'>
- <button>Delete</button>
- </form>
- <form action='#{File.join "boost", file}' method='post'>
- <button>Boost</button>
- </form>
- <form action='#{File.join "archive", file}' method='post'>
- <button>Archive</button>
- </form>
- <form action='#{File.join "reply", file}' method='post'>
- <button>Reply</button>
- </form>
- <hr>"
- end
- html << "\n\t</body>\n</html>"
- end
-=begin
-=end
-
-=begin
- def html o
- html = "<!DOCTYPE html>
-<html lang='en'>
- <body>
- <b>#{mention o["actor"]}</b>&nbsp;<i>#{o["object"]["published"]}</i>
- <p>#{o["object"]["content"]}
- "
- if o["object"]["attachment"]
- o["object"]["attachment"].each do |att|
- case att["mediaType"]
- when /audio/
- html<< "\n<br><audio controls=''><source src='#{att["url"]}' type='#{att["mediaType"]}'></audio>"
- when /image/
- html << "\n<br><a href='#{att["url"]}'><img src='#{att["url"]}'></a>"
- when /video/
- html<< "\n<br><video controls=''><source src='#{att["url"]}' type='#{att["mediaType"]}'></video>"
- else
- html<< att + "<br>"
- html << "\n<a href='#{att["url"]}'>#{att["url"]}</a>"
- end
- end
- end
- end
- html << "\n\t</body>\n</html>"
- html
- end
-=end
-
- def parse input
- date = Time.now.strftime("%Y-%m-%dT%H:%M:%S")
- # 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" => "",
- "to" => ["https://www.w3.org/ns/activitystreams#Public"]
- }
- create = {
- "@context" => "https://www.w3.org/ns/activitystreams",
- "id" => File.join(SOCIAL_URL, "create", SecureRandom.uuid + ".json"),
- "type" => "Create",
- "actor" => ACTOR,
- "object" => note,
- "published" => date,
- "to" => ["https://www.w3.org/ns/activitystreams#Public"]
- }
- 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 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 mention actor
- "#{get(actor)["preferredUsername"]}@#{URI(actor).host}"
- end
-
- def send 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)
-
- sha256 = OpenSSL::Digest::SHA256.new
- body = object.to_json
- digest = "SHA-256=" + sha256.base64digest(body)
-
- signed_string = "(request-target): post #{inbox uri}\nhost: #{uri.host}\ndate: #{date}\ndigest: #{digest}\ncontent-type: application/activity+json"
- signature = Base64.strict_encode64(keypair.sign(OpenSSL::Digest.new('SHA256'), signed_string))
- signed_header = 'keyId="' + ACTOR + '#main-key",algorithm="rsa-sha256",headers="(request-target) host date digest content-type",signature="' + signature + '"'
-
- uri = URI.parse(get(url)["inbox"])
- http = Net::HTTP.new(uri.host, uri.port)
- http.use_ssl = true
- header = {
- 'Content-Type' => 'application/activity+json',
- 'Host' => uri.host,
- 'Date' => date,
- 'Digest' => digest,
- 'Signature' => signed_header,
- }
- request = Net::HTTP::Post.new(uri.request_uri, header)
- request.body = body
-
- responses << http.request(request)
- end
- # puts responses
- responses
- end
-
- def ordered_collection dir
- collection = dir.sub(/^\//, "")
- posts = Dir[File.join(collection, "*.json")].collect { |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,
- }
- 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
- signature_params = {}
- env["HTTP_SIGNATURE"].split(',').each do |pair|
- k, v = pair.split('=')
- signature_params[k] = v.gsub('"', '')
- end
-
- key_id = signature_params['keyId']
- headers = signature_params['headers']
- signature = Base64.decode64(signature_params['signature'])
-
- actor = get key_id
- key = OpenSSL::PKey::RSA.new(actor['publicKey']['publicKeyPem'])
-
- comparison = headers.split(' ').map do |signed_params_name|
- if signed_params_name == '(request-target)'
- '(request-target): post /inbox'
- elsif signed_params_name == 'content-type'
- "#{signed_params_name}: #{env["CONTENT_TYPE"]}"
- else
- "#{signed_params_name}: #{env["HTTP_" + signed_params_name.upcase]}"
- end
- end.join("\n")
-
- key.verify(OpenSSL::Digest.new('SHA256'), signature, comparison)
- end
-
- def auth env
- 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]
- true
- end
-end