gem 'http'
gem 'addressable'
gem 'nokogiri'
+gem 'link_header'
gem 'ostatus2'
gem 'goldfinger'
gem 'devise'
letter_opener (1.4.1)
launchy (~> 2.2)
libv8 (3.16.14.15)
+ link_header (0.0.8)
lograge (0.4.1)
actionpack (>= 4, < 5.1)
activesupport (>= 4, < 5.1)
jbuilder (~> 2.0)
jquery-rails
letter_opener
+ link_header
lograge
nokogiri
oj
layout 'public'
before_action :set_account
- before_action :set_webfinger_header
+ before_action :set_link_headers
def show
respond_to do |format|
@account = Account.find_local!(params[:username])
end
- def set_webfinger_header
- response.headers['Link'] = "<#{webfinger_account_url}>; rel=\"lrdd\"; type=\"application/xrd+xml\""
+ def set_link_headers
+ response.headers['Link'] = LinkHeader.new([
+ [webfinger_account_url, [['rel', 'lrdd'], ['type', 'application/xrd+xml']]],
+ [account_url(@account, format: 'atom'), [['rel', 'alternate'], ['type', 'application/atom+xml']]]
+ ])
end
def webfinger_account_url
before_action :set_account
before_action :set_stream_entry
+ before_action :set_link_headers
def show
@type = @stream_entry.activity_type.downcase
@account = Account.find_local!(params[:account_username])
end
+ def set_link_headers
+ response.headers['Link'] = LinkHeader.new([
+ [account_stream_entry_url(@account, @stream_entry, format: 'atom'), [['rel', 'alternate'], ['type', 'application/atom+xml']]]
+ ])
+ end
+
def set_stream_entry
@stream_entry = @account.stream_entries.find(params[:id])
end
--- /dev/null
+class FetchRemoteStatusService < BaseService
+ def call(url)
+ response = http_client.head(url)
+
+ Rails.logger.debug "Remote status HEAD request returned code #{response.code}"
+ return nil if response.code != 200
+
+ if response.mime_type == 'application/atom+xml'
+ return process_atom(url, fetch(url))
+ elsif !response['Link'].blank?
+ return process_headers(response)
+ else
+ return process_html(fetch(url))
+ end
+ end
+
+ private
+
+ def process_atom(url, body)
+ Rails.logger.debug "Processing Atom for remote status"
+
+ xml = Nokogiri::XML(body)
+ account = extract_author(url, xml)
+
+ return nil if account.nil?
+
+ statuses = ProcessFeedService.new.(body, account)
+
+ return statuses.first
+ end
+
+ def process_html(body)
+ Rails.logger.debug "Processing HTML for remote status"
+
+ page = Nokogiri::HTML(body)
+ alternate_link = page.xpath('//link[@rel="alternate"]').find { |link| link['type'] == 'application/atom+xml' }
+
+ return nil if alternate_link.nil?
+ return process_atom(alternate_link['href'], fetch(alternate_link['href']))
+ end
+
+ def process_headers(response)
+ Rails.logger.debug "Processing link header for remote status"
+
+ link_header = LinkHeader.parse(response['Link'])
+ alternate_link = link_header.find_link(['rel', 'alternate'], ['type', 'application/atom+xml'])
+
+ return nil if alternate_link.nil?
+ return process_atom(alternate_link.href, fetch(alternate_link.href))
+ end
+
+ def extract_author(url, xml)
+ url_parts = Addressable::URI.parse(url)
+ username = xml.at_xpath('//xmlns:author/xmlns:name').try(:content)
+ domain = url_parts.host
+
+ return nil if username.nil?
+
+ Rails.logger.debug "Going to webfinger #{username}@#{domain}"
+
+ return FollowRemoteAccountService.new.("#{username}@#{domain}")
+ end
+
+ def fetch(url)
+ http_client.get(url).to_s
+ end
+
+ def http_client
+ HTTP.timeout(:per_operation, write: 20, connect: 20, read: 50)
+ end
+end
end
def http_client
- HTTP
+ HTTP.timeout(:per_operation, write: 20, connect: 20, read: 50)
end
end
# Create local statuses from an Atom feed
# @param [String] body Atom feed
# @param [Account] account Account this feed belongs to
+ # @return [Enumerable] created statuses
def call(body, account)
xml = Nokogiri::XML(body)
update_remote_profile_service.(xml.at_xpath('/xmlns:feed/xmlns:author'), account) unless xml.at_xpath('/xmlns:feed').nil?
- xml.xpath('//xmlns:entry').each { |entry| process_entry(account, entry) }
+ xml.xpath('//xmlns:entry').reverse_each.map { |entry| process_entry(account, entry) }.compact
end
private
DistributionWorker.perform_async(status.id)
end
+
+ return status
end
def record_remote_mentions(status, links)
def add_reply!(entry, status)
status.thread = find_original_status(entry, thread_id(entry))
status.save!
+
+ if status.thread.nil? && !thread_href(entry).nil?
+ ThreadResolveWorker.perform_async(status.id, thread_href(entry))
+ end
end
def delete_post!(status)
status = Status.new(account: account, uri: target_id(xml), text: target_content(xml), url: target_url(xml), created_at: published(xml), updated_at: updated(xml))
status.thread = find_original_status(xml, thread_id(xml))
+ status.save
+
+ if status.saved? && status.thread.nil? && !thread_href(xml).nil?
+ ThreadResolveWorker.perform_async(status.id, thread_href(xml))
+ end
+
+ status
rescue Goldfinger::Error, HTTP::Error
nil
end
nil
end
+ def thread_href(xml)
+ xml.at_xpath('./thr:in-reply-to').attribute('href').value
+ rescue
+ nil
+ end
+
def target_id(xml)
xml.at_xpath('.//activity:object/xmlns:id').content
rescue
--- /dev/null
+class ThreadResolveWorker
+ include Sidekiq::Worker
+
+ def perform(child_status_id, parent_url)
+ child_status = Status.find(child_status_id)
+ parent_status = FetchRemoteStatusService.new.(parent_url)
+
+ unless parent_status.nil?
+ child_status.thread = parent_status
+ child_status.save!
+ end
+ end
+end
before do
stub_request(:get, "https://quitter.no/avatar/7477-300-20160211190340.png").to_return(request_fixture('avatar.txt'))
+ stub_request(:head, "https://quitter.no/notice/1269244").to_return(status: 404)
+ stub_request(:head, "https://quitter.no/notice/1265331").to_return(status: 404)
+ stub_request(:head, "https://community.highlandarrow.com/notice/54411").to_return(status: 404)
+ stub_request(:head, "https://community.highlandarrow.com/notice/53857").to_return(status: 404)
+ stub_request(:head, "https://community.highlandarrow.com/notice/51852").to_return(status: 404)
+ stub_request(:head, "https://social.umeahackerspace.se/notice/424348").to_return(status: 404)
+ stub_request(:head, "https://community.highlandarrow.com/notice/50467").to_return(status: 404)
+ stub_request(:head, "https://quitter.no/notice/1243309").to_return(status: 404)
request.env['HTTP_X_HUB_SIGNATURE'] = "sha1=#{OpenSSL::HMAC.hexdigest('sha1', 'abc', feed)}"
request.env['RAW_POST_DATA'] = feed