require 'English' helpers do def save_item(item, path) FileUtils.mkdir_p File.dirname(path) File.open(path, 'w+') { |f| f.puts item.to_json } end # add date and id, save def save_activity(activity, box) date = Time.now.utc.iso8601 activity['published'] ||= date # if box == OUTBOX basename = "#{activity['published']}_#{mention(activity['actor'])}.json" 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 # save object save_object activity['object'], box if %w[Create Announce Update].include? activity['type'] end # save activity FileUtils.mkdir_p File.dirname(activity_path) File.open(activity_path, 'w+') { |f| f.puts activity.to_json } activity_path end def save_object(object, box) object = fetch(object) if object.is_a? String and object.match(/^http/) return unless object and object['type'] != 'Person' return if box == INBOX and object['id'] and File.readlines(VISITED, chomp: true).include? object['id'] object['@context'] = 'https://www.w3.org/ns/activitystreams' if object['attributedTo'] basename = "#{object['published']}_#{mention(object['attributedTo'])}.json" else basename = "#{object['published']}.json" jj object end object_rel_path = File.join 'object', object['type'].downcase, basename 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 } if box == OUTBOX and object['tag'] object['tag'].each do |tag| next unless tag['type'] == 'Hashtag' tag_path = File.join(TAGS[:dir], tag['name'].sub('#', '')) + '.json' tag_collection = if File.exist? tag_path JSON.load_file(tag_path) else { '@context' => 'https://www.w3.org/ns/activitystreams', 'id' => tag['href'], 'type' => 'OrderedCollection', 'totalItems' => 0, 'orderedItems' => [] } end tag_collection['orderedItems'] << object['id'] tag_collection['totalItems'] = tag_collection['orderedItems'].size File.open(tag_path, 'w+') do |f| f.puts tag_collection.to_json end end elsif box == INBOX File.open(File.join(INBOX[:dir], 'visited'), 'a+') { |f| f.puts object['id'] } end object 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') begin uri = URI(url) rescue StandardError => e p url, e return nil end 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) response = `/run/current-system/sw/bin/curl -H 'Content-Type: #{CONTENT_TYPE}' -H 'Accept: #{CONTENT_TYPE}' --fail-with-body -sSL #{ext} #{url}` if $CHILD_STATUS.success? response else p url, 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('public/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('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 webp], 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 def find_file(id) Dir[File.join('*', 'object', '*', '*.json')].find do |f| JSON.load_file(f)['id'] == id end end def find_id(id, return_filename = true) Dir[File.join('**', '*.json')].find do |f| content = JSON.load_file(f) if content['id'] == id return_filename ? f : content end end end end