# frozen_string_literal: true require 'English' helpers do def outbox(type, object, recipients, add_recipients = false) activity = { '@context' => 'https://www.w3.org/ns/activitystreams', 'type' => type, 'actor' => ACTOR, 'object' => object } now = Time.now date = now.strftime('%Y-%m-%dT%H:%M:%S.%N') httpdate = now.utc.httpdate basename = "#{date}.json" activity_rel_path = File.join(type.downcase, basename) activity_path = File.join(OUTBOX_DIR, activity_rel_path) activity['id'] = File.join(OUTBOX_URL, activity_rel_path) activity['published'] = httpdate # assumes that recipient collections have been expanded by sender # put all recipients into 'to', avoid 'cc' 'bto' 'bcc' 'audience' !! activity['to'] = recipients if add_recipients # save object if activity['object'] && activity['object']['type'] && !activity['object']['id'] object = activity['object'] object['@context'] = 'https://www.w3.org/ns/activitystreams' object_rel_path = File.join activity['object']['type'].downcase, basename object_path = File.join OUTBOX_DIR, object_rel_path object['id'] = File.join OUTBOX_URL, object_rel_path object['published'] = httpdate FileUtils.mkdir_p File.dirname(object_path) File.open(object_path, 'w+') { |f| f.puts object.to_json } end # save activity FileUtils.mkdir_p File.dirname(activity_path) File.open(activity_path, 'w+') { |f| f.puts activity.to_json } update_collection OUTBOX, activity['id'] if %w[Create Announce].include?(type) # send # https://github.com/mastodon/mastodon/blob/main/app/lib/request.rb keypair = OpenSSL::PKey::RSA.new(File.read('private.pem')) body = activity.to_json sha256 = OpenSSL::Digest.new('SHA256') digest = "SHA-256=#{sha256.base64digest(body)}" inboxes = [] recipients.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'] end end inboxes.compact.uniq.each do |inbox| uri = URI(inbox) string = "(request-target): post #{uri.request_uri}\nhost: #{uri.host}\ndate: #{httpdate}\ndigest: #{digest}\ncontent-type: application/activity+json" signature = Base64.strict_encode64(keypair.sign(OpenSSL::Digest.new('SHA256'), string)) signed_header = "keyId=\"#{ACTOR}#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"#{signature}\"" curl( "-X POST -H 'Content-Type: application/activity+json' -H 'Host: #{uri.host}' -H 'Date: #{httpdate}' -H 'Digest: #{digest}' -H 'Signature: #{signed_header}' -d '#{body}'", inbox ) end activity end def update_collection(path, objects, delete = false) objects = [objects] unless objects.is_a? Array File.open(path, 'r+') do |f| f.flock(File::LOCK_EX) json = f.read collection = JSON.parse(json) objects.each do |object| id = object['id'] || object if delete collection['orderedItems'].delete_if { |o| o['id'] == id or o == id } else ids = collection['orderedItems'].collect { |i| i['id'] } collection['orderedItems'] << object unless ids.include?(id) or collection['orderedItems'].include?(id) end end collection['totalItems'] = collection['orderedItems'].size f.rewind f.puts collection.to_json f.truncate(f.pos) end end def fetch(url, accept = 'application/activity+json') uri = URI(url) httpdate = Time.now.utc.httpdate keypair = OpenSSL::PKey::RSA.new(File.read('private.pem')) string = "(request-target): get #{uri.request_uri}\nhost: #{uri.host}\ndate: #{httpdate}" signature = Base64.strict_encode64(keypair.sign(OpenSSL::Digest.new('SHA256'), string)) signed_header = "keyId=\"#{ACTOR}#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date\",signature=\"#{signature}\"" response = curl( "-H 'Accept: #{accept}' -H 'Host: #{uri.host}' -H 'Date: #{httpdate}' -H 'Signature: #{signed_header}' ", url ) response ? JSON.parse(response) : nil end def curl(ext, url) p url # p "/run/current-system/sw/bin/curl --fail-with-body -sSL #{ext} #{url}" response = `/run/current-system/sw/bin/curl --fail-with-body -sSL #{ext} #{url}` if $CHILD_STATUS.success? response else p response nil end end def mention(actor) person = people.select { |p| p[1] == actor } if person.empty? a = fetch(actor) return nil unless a mention = "#{a['preferredUsername']}@#{URI(actor).host}" cache mention, actor, a mention else person[0][0] end end def actor(mention) mention = mention.sub(/^@/, '').chomp actors = people.select { |p| p[0] == mention } if actors.empty? _, server = mention.split('@') a = fetch("https://#{server}/.well-known/webfinger?resource=acct:#{mention}", 'application/jrd+json') return nil unless a actor = a['links'].select do |l| l['rel'] == 'self' end[0]['href'] cache mention, actor, a actor else actors[0][1] end end def people File.read('private/people.tsv').split("\n").collect { |l| l.chomp.split("\t") } end def cache(mention, actor, a) sharedInbox = a['endpoints']['sharedInbox'] if a['endpoints'] && a['endpoints']['sharedInbox'] File.open('private/people.tsv', 'a') { |f| f.puts "#{mention}\t#{actor}\t#{sharedInbox}" } end end