diff options
-rw-r--r-- | activitypub.rb | 3 | ||||
-rw-r--r-- | client.rb | 3 | ||||
-rw-r--r-- | create.rb | 97 | ||||
-rw-r--r-- | helpers.rb | 19 | ||||
-rw-r--r-- | send.rb | 76 | ||||
-rw-r--r-- | server.rb | 36 |
6 files changed, 142 insertions, 92 deletions
diff --git a/activitypub.rb b/activitypub.rb index 90fe0c4..8a62fdd 100644 --- a/activitypub.rb +++ b/activitypub.rb @@ -22,6 +22,7 @@ ACTOR = File.join(SOCIAL_URL, USER) 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') } +FOLLOWERS_URL = 'https://social.pdp8.info/followers' CONTENT_TYPE = 'application/activity+json' # CONTENT_TYPE = 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' @@ -34,4 +35,4 @@ set :port, 9292 require_relative 'helpers' require_relative 'server' require_relative 'client' -require_relative 'send' +require_relative 'create' @@ -45,6 +45,7 @@ post '/follow' do protected! params['id'] = actor params['mention'] if params['mention'] outbox 'Follow', params['id'], [params['id']] + 200 end post '/unfollow' do @@ -55,6 +56,7 @@ post '/unfollow' do activity ||= save_activity({ 'type' => 'Follow', 'actor' => ACTOR, 'object' => params['id'] }, OUTBOX) # recreate activity for old/deleted follows outbox 'Undo', activity, [params['id']] update_collection FOLLOWING, params['id'], true + 200 end post '/share' do # TODO @@ -72,6 +74,7 @@ end post '/login' do session['client'] = (OpenSSL::Digest::SHA256.base64digest(params['secret']) == File.read('.digest').chomp) + 200 end helpers do diff --git a/create.rb b/create.rb new file mode 100644 index 0000000..d986812 --- /dev/null +++ b/create.rb @@ -0,0 +1,97 @@ +post '/create' do # TODO + protected! + request.body.rewind # in case someone already read it + + to = [] + inReplyTo = '' + content = [] + tag = [] + attachment = [] + + url_regexp = %r{\Ahttps?://\S+\Z} + mention_regexp = /\A@?\w+@\S+\Z/ + hashtag_regexp = /\A#\w+\Z/ + + lines = request.body.read.each_line.to_a + lines.each.with_index do |line, i| + line.chomp! + if i == 0 + to = line.split(/\s+/).collect do |word| + case word + when 'public' + ['https://www.w3.org/ns/activitystreams#Public', FOLLOWERS_URL] + when mention_regexp + actor word + when url_regexp + word + end + end.flatten + elsif i == 1 and line.match url_regexp + inReplyTo = line + elsif line == '' + content << '<p>' + elsif line.match(/\A==\Z/) + attachment = lines[i + 1..-1].collect do |url| + url.chomp! + { + 'type' => 'Document', + 'mediaType' => media_type(url), + 'url' => url + } + end + break + else + tags = line.split(/\s+/).grep(hashtag_regexp) + 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(mention_regexp) + mentions.each do |mention| + actor = actor(mention) + tag << { + 'type' => 'Mention', + 'href' => actor, + 'name' => mention + } + end + content << line + end + end + + object = { + 'to' => to, + 'type' => 'Note', + 'attributedTo' => ACTOR, + 'content' => "#{content.join("\n")}" + } + object['inReplyTo'] = inReplyTo unless inReplyTo.empty? + object['attachment'] = attachment unless attachment.empty? + object['tag'] = tag unless tag.empty? + + activity = outbox 'Create', object, to + if activity['object']['tag'] + 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 + end + end + 200 +end @@ -11,11 +11,13 @@ helpers do activity_rel_path = File.join(activity['type'].downcase, basename) activity_path = File.join(box[:dir], activity_rel_path) if box == OUTBOX + return unless activity['to'].include? 'https://www.w3.org/ns/activitystreams#Public' # save only public messages + activity['id'] = File.join(box[:url], activity_rel_path) activity['object']['published'] = date unless activity['object'].is_a? String end # save object - save_object activity['object'], box if activity['object'] && activity['object']['type'] && !activity['object']['id'] + save_object activity['object'], box if %w[Create Announce Update].include? activity['type'] # save activity FileUtils.mkdir_p File.dirname(activity_path) File.open(activity_path, 'w+') { |f| f.puts activity.to_json } @@ -24,12 +26,12 @@ helpers do def save_object(object, box) object = fetch(object) if object.is_a? String and object.match(/^http/) - return unless object + return unless object # and object['type'] != 'Person' 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 - object['id'] = File.join box[:url], object_rel_path if box == OUTBOX + object['id'] ||= File.join box[:url], object_rel_path # if box == OUTBOX object_path = File.join box[:dir], object_rel_path FileUtils.mkdir_p File.dirname(object_path) File.open(object_path, 'w+') { |f| f.puts object.to_json } @@ -122,4 +124,15 @@ helpers do sharedInbox = a['endpoints']['sharedInbox'] if a['endpoints'] && a['endpoints']['sharedInbox'] File.open('public/people.tsv', 'a') { |f| f.puts "#{mention}\t#{actor}\t#{sharedInbox}" } end + + def media_type(url) # TODO: extend extensions + extensions = { + image: %w[jpeg jpg png tiff], + audio: %w[flac wav mp3 ogg aiff], + video: %w[mp4 webm] + } + ext = File.extname(url).sub('.', '').downcase + type = extensions.find { |_k, v| v.include? ext } + "#{type[0]}/#{ext}" + end end diff --git a/send.rb b/send.rb deleted file mode 100644 index 6dd4f3b..0000000 --- a/send.rb +++ /dev/null @@ -1,76 +0,0 @@ -post '/outbox' do # TODO - protected! - - recipients = params[:public] == 'true' ? public : [] - recipients << params[:to] - - tag = [] - params[:content].lines.each do |line| - 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 - end - - attachment = [] - extensions = { - image: %w[jpeg png], - audio: %w[flac wav mp3 ogg], - video: %w[mp4 webm] - } - if params[:media] - 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 - end - - object = { - 'type' => 'Note', - 'attributedTo' => ACTOR, - 'inReplyTo' => params[:inReplyTo], - 'content' => "<p>\n#{params[:content]}\n</p>", - 'attachment' => attachment, - 'tag' => tag, - 'to' => recipients - } - - activity = outbox 'Create', object, recipients - 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 - end -end @@ -13,8 +13,8 @@ post '/inbox' do end halt 501 if @activity['actor'] and @activity['type'] == 'Delete' # deleted actors return 403 => verification error # verify! # pixelfed sends unsigned activities??? - save_activity(@activity, INBOX) type = @activity['type'].downcase.to_sym + save_activity(@activity, INBOX) unless %i[create announce].include? type send(type) if %i[create announce follow accept undo].include? type halt 200 end @@ -124,21 +124,32 @@ helpers do halt 403 unless key.verify(OpenSSL::Digest.new('SHA256'), signature, comparison) end - def outbox(type, object, recipients) + def actor_inbox(url) + actor = fetch url + return unless actor + + if actor['endpoints'] and actor['endpoints']['sharedInbox'] + actor['endpoints']['sharedInbox'] + elsif actor['inbox'] + actor['inbox'] + end + end + + def outbox(type, object, to) # send ## https://github.com/mastodon/mastodon/blob/main/app/lib/request.rb + to = [to] if to.is_a?(String) inboxes = [] - recipients.uniq.each do |url| + to.uniq.each do |url| next if [ACTOR, 'https://www.w3.org/ns/activitystreams#Public'].include? url - actor = fetch url - next unless actor - - if actor['endpoints'] and actor['endpoints']['sharedInbox'] - inboxes << actor['endpoints']['sharedInbox'] - elsif actor['inbox'] - inboxes << actor['inbox'] + if url == FOLLOWERS_URL + JSON.parse(File.read(FOLLOWERS))['orderedItems'].each do |follower| + inboxes << actor_inbox(follower) + end + next end + inboxes << actor_inbox(url) end # add date and id, save @@ -147,8 +158,9 @@ helpers do 'type' => type, 'actor' => ACTOR, 'object' => object, - 'to' => recipients + 'to' => to }, OUTBOX) + body = activity.to_json sha256 = OpenSSL::Digest.new('SHA256') digest = "SHA-256=#{sha256.base64digest(body)}" @@ -162,7 +174,7 @@ helpers do signed_header = "keyId=\"#{ACTOR}#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"#{signature}\"" curl( - "-X POST -H 'Host: #{uri.host}' -H 'Date: #{httpdate}' -H 'Digest: #{digest}' -H 'Signature: #{signed_header}' --data-binary '#{body}'", inbox + "-X POST -H 'Host: #{uri.host}' -H 'Date: #{httpdate}' -H 'Digest: #{digest}' -H 'Signature: #{signed_header}' --data-raw '#{body}'", inbox ) end activity |