summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorpdp8 <pdp8@pdp8.info>2023-05-19 14:18:03 +0200
committerpdp8 <pdp8@pdp8.info>2023-05-19 14:18:03 +0200
commitd523ce8d4fe4e852944331eb0c300d309641c057 (patch)
tree1d59b80cbf4216310b3ef08ab772c6383d4297c4
parentfb5068619adfd715e2e1e72bede45ed83b28ee1c (diff)
input parsing @ server, deliver to followers, simple content formatting
-rw-r--r--.gitignore2
-rw-r--r--application.rb150
-rwxr-xr-xclient.rb72
3 files changed, 185 insertions, 39 deletions
diff --git a/.gitignore b/.gitignore
index cfaad76..90fa493 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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)