Merge tag 'v3.1.1' into instance_only_statuses

This commit is contained in:
Renato "Lond" Cerqueira
2020-02-21 14:21:59 +01:00
1431 changed files with 34462 additions and 10030 deletions

View File

@ -4,8 +4,8 @@ class AccountSearchService < BaseService
attr_reader :query, :limit, :offset, :options, :account
def call(query, account = nil, options = {})
@acct_hint = query.start_with?('@')
@query = query.strip.gsub(/\A@/, '')
@acct_hint = query&.start_with?('@')
@query = query&.strip&.gsub(/\A@/, '')
@limit = options[:limit].to_i
@offset = options[:offset].to_i
@options = options
@ -127,7 +127,7 @@ class AccountSearchService < BaseService
end
def following_ids
@following_ids ||= account.active_relationships.pluck(:target_account_id)
@following_ids ||= account.active_relationships.pluck(:target_account_id) + [account.id]
end
def limit_for_non_exact_results

View File

@ -37,7 +37,7 @@ class ActivityPub::ProcessCollectionService < BaseService
end
def process_item(item)
activity = ActivityPub::Activity.factory(item, @account, @options)
activity = ActivityPub::Activity.factory(item, @account, **@options)
activity&.perform
end

View File

@ -30,7 +30,7 @@ class ActivityPub::ProcessPollService < BaseService
voters_count = @json['votersCount']
latest_options = items.map { |item| item['name'].presence || item['content'] }
latest_options = items.map { |item| item['name'].presence || item['content'] }.compact
# If for some reasons the options were changed, it invalidates all previous
# votes, so we need to remove them

View File

@ -3,6 +3,8 @@
require 'rubygems/package'
class BackupService < BaseService
include Payloadable
attr_reader :account, :backup, :collection
def call(backup)
@ -20,7 +22,7 @@ class BackupService < BaseService
account.statuses.with_includes.reorder(nil).find_in_batches do |statuses|
statuses.each do |status|
item = serialize(status, ActivityPub::ActivitySerializer)
item = serialize_payload(status, ActivityPub::ActivitySerializer, signer: @account)
item.delete(:'@context')
unless item[:type] == 'Announce' || item[:object][:attachment].blank?
@ -45,6 +47,7 @@ class BackupService < BaseService
dump_media_attachments!(tar)
dump_outbox!(tar)
dump_likes!(tar)
dump_bookmarks!(tar)
dump_actor!(tar)
end
end
@ -85,6 +88,7 @@ class BackupService < BaseService
actor[:image][:url] = 'header' + File.extname(actor[:image][:url]) if actor[:image]
actor[:outbox] = 'outbox.json'
actor[:likes] = 'likes.json'
actor[:bookmarks] = 'bookmarks.json'
download_to_tar(tar, account.avatar, 'avatar' + File.extname(account.avatar.path)) if account.avatar.exists?
download_to_tar(tar, account.header, 'header' + File.extname(account.header.path)) if account.header.exists?
@ -115,6 +119,25 @@ class BackupService < BaseService
end
end
def dump_bookmarks!(tar)
collection = serialize(ActivityPub::CollectionPresenter.new(id: 'bookmarks.json', type: :ordered, size: 0, items: []), ActivityPub::CollectionSerializer)
Status.reorder(nil).joins(:bookmarks).includes(:account).merge(account.bookmarks).find_in_batches do |statuses|
statuses.each do |status|
collection[:totalItems] += 1
collection[:orderedItems] << ActivityPub::TagManager.instance.uri_for(status)
end
GC.start
end
json = Oj.dump(collection)
tar.add_file_simple('bookmarks.json', 0o444, json.bytesize) do |io|
io.write(json)
end
end
def collection_presenter
ActivityPub::CollectionPresenter.new(
id: 'outbox.json',
@ -142,7 +165,7 @@ class BackupService < BaseService
io.write(buffer)
end
end
rescue Errno::ENOENT
rescue Errno::ENOENT, Seahorse::Client::NetworkingError
Rails.logger.warn "Could not backup file #{filename}: file not found"
end
end

View File

@ -20,13 +20,13 @@ class BlockDomainService < BaseService
end
def process_domain_block!
clear_media! if domain_block.reject_media?
if domain_block.silence?
silence_accounts!
elsif domain_block.suspend?
suspend_accounts!
end
clear_media! if domain_block.reject_media?
end
def invalidate_association_caches!

View File

@ -5,7 +5,7 @@ class BootstrapTimelineService < BaseService
@source_account = source_account
autofollow_inviter!
autofollow_bootstrap_timeline_accounts!
autofollow_bootstrap_timeline_accounts! if Setting.enable_bootstrap_timeline_accounts
end
private

View File

@ -39,7 +39,13 @@ class FetchLinkCardService < BaseService
def process_url
@card ||= PreviewCard.new(url: @url)
Request.new(:get, @url).perform do |res|
attempt_oembed || attempt_opengraph
end
def html
return @html if defined?(@html)
Request.new(:get, @url).add_headers('Accept' => 'text/html').perform do |res|
if res.code == 200 && res.mime_type == 'text/html'
@html = res.body_with_limit
@html_charset = res.charset
@ -48,10 +54,6 @@ class FetchLinkCardService < BaseService
@html_charset = nil
end
end
return if @html.nil?
attempt_oembed || attempt_opengraph
end
def attach_card
@ -65,7 +67,7 @@ class FetchLinkCardService < BaseService
else
html = Nokogiri::HTML(@status.text)
links = html.css('a')
urls = links.map { |a| Addressable::URI.parse(a['href']).normalize unless skip_link?(a) }.compact
urls = links.map { |a| Addressable::URI.parse(a['href']) unless skip_link?(a) }.compact.map(&:normalize).compact
end
urls.reject { |uri| bad_url?(uri) }.first
@ -84,16 +86,21 @@ class FetchLinkCardService < BaseService
def skip_link?(a)
# Avoid links for hashtags and mentions (microformats)
a['rel']&.include?('tag') || a['class']&.include?('u-url') || mention_link?(a)
a['rel']&.include?('tag') || a['class']&.match?(/u-url|h-card/) || mention_link?(a)
end
def attempt_oembed
service = FetchOEmbedService.new
embed = service.call(@url, html: @html)
url = Addressable::URI.parse(service.endpoint_url)
service = FetchOEmbedService.new
url_domain = Addressable::URI.parse(@url).normalized_host
cached_endpoint = Rails.cache.read("oembed_endpoint:#{url_domain}")
embed = service.call(@url, cached_endpoint: cached_endpoint) unless cached_endpoint.nil?
embed ||= service.call(@url, html: html) unless html.nil?
return false if embed.nil?
url = Addressable::URI.parse(service.endpoint_url)
@card.type = embed[:type]
@card.title = embed[:title] || ''
@card.author_name = embed[:author_name] || ''
@ -127,6 +134,8 @@ class FetchLinkCardService < BaseService
end
def attempt_opengraph
return if html.nil?
detector = CharlockHolmes::EncodingDetector.new
detector.strip_tags = true

View File

@ -1,13 +1,20 @@
# frozen_string_literal: true
class FetchOEmbedService
ENDPOINT_CACHE_EXPIRES_IN = 24.hours.freeze
attr_reader :url, :options, :format, :endpoint_url
def call(url, options = {})
@url = url
@options = options
discover_endpoint!
if @options[:cached_endpoint]
parse_cached_endpoint!
else
discover_endpoint!
end
fetch!
end
@ -32,10 +39,32 @@ class FetchOEmbedService
return if @endpoint_url.blank?
@endpoint_url = (Addressable::URI.parse(@url) + @endpoint_url).to_s
cache_endpoint!
rescue Addressable::URI::InvalidURIError
@endpoint_url = nil
end
def parse_cached_endpoint!
cached = @options[:cached_endpoint]
return if cached[:endpoint].nil? || cached[:format].nil?
@endpoint_url = Addressable::Template.new(cached[:endpoint]).expand(url: @url).to_s
@format = cached[:format]
end
def cache_endpoint!
url_domain = Addressable::URI.parse(@url).normalized_host
endpoint_hash = {
endpoint: @endpoint_url.gsub(/(=(http[s]?(%3A|:)(\/\/|%2F%2F)))([^&]*)/i, '={url}'),
format: @format,
}
Rails.cache.write("oembed_endpoint:#{url_domain}", endpoint_hash, expires_in: ENDPOINT_CACHE_EXPIRES_IN)
end
def fetch!
return if @endpoint_url.blank?
@ -64,7 +93,7 @@ class FetchOEmbedService
def html
return @html if defined?(@html)
@html = @options[:html] || Request.new(:get, @url).perform do |res|
@html = @options[:html] || Request.new(:get, @url).add_headers('Accept' => 'text/html').perform do |res|
res.code != 200 || res.mime_type != 'text/html' ? nil : res.body_with_limit
end
end

View File

@ -1,17 +0,0 @@
# frozen_string_literal: true
class FetchRemoteAccountService < BaseService
def call(url, prefetched_body = nil, protocol = :ostatus)
if prefetched_body.nil?
resource_url, resource_options, protocol = FetchResourceService.new.call(url)
else
resource_url = url
resource_options = { prefetched_body: prefetched_body }
end
case protocol
when :activitypub
ActivityPub::FetchRemoteAccountService.new.call(resource_url, **resource_options)
end
end
end

View File

@ -1,17 +1,14 @@
# frozen_string_literal: true
class FetchRemoteStatusService < BaseService
def call(url, prefetched_body = nil, protocol = :ostatus)
def call(url, prefetched_body = nil)
if prefetched_body.nil?
resource_url, resource_options, protocol = FetchResourceService.new.call(url)
resource_url, resource_options = FetchResourceService.new.call(url)
else
resource_url = url
resource_options = { prefetched_body: prefetched_body }
end
case protocol
when :activitypub
ActivityPub::FetchRemoteStatusService.new.call(resource_url, **resource_options)
end
ActivityPub::FetchRemoteStatusService.new.call(resource_url, **resource_options) unless resource_url.nil?
end
end

View File

@ -3,7 +3,7 @@
class FetchResourceService < BaseService
include JsonLdHelper
ACCEPT_HEADER = 'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams", text/html'
ACCEPT_HEADER = 'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams", text/html;q=0.1'
def call(url)
return if url.blank?
@ -33,7 +33,7 @@ class FetchResourceService < BaseService
body = response.body_with_limit
json = body_to_json(body)
[json['id'], { prefetched_body: body, id: true }, :activitypub] if supported_context?(json) && (equals_or_includes_any?(json['type'], ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES) || expected_type?(json))
[json['id'], { prefetched_body: body, id: true }] if supported_context?(json) && (equals_or_includes_any?(json['type'], ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES) || expected_type?(json))
elsif !terminal
link_header = response['Link'] && parse_link_header(response)

View File

@ -8,7 +8,7 @@ class FollowService < BaseService
# @param [Account] source_account From which to follow
# @param [String, Account] uri User URI to follow in the form of username@domain (or account record)
# @param [true, false, nil] reblogs Whether or not to show reblogs, defaults to true
def call(source_account, target_account, reblogs: nil)
def call(source_account, target_account, reblogs: nil, bypass_locked: false)
reblogs = true if reblogs.nil?
target_account = ResolveAccountService.new.call(target_account, skip_webfinger: true)
@ -30,7 +30,7 @@ class FollowService < BaseService
ActivityTracker.increment('activity:interactions')
if target_account.locked? || source_account.silenced? || target_account.activitypub?
if (target_account.locked? && !bypass_locked) || source_account.silenced? || target_account.activitypub?
request_follow(source_account, target_account, reblogs: reblogs)
elsif target_account.local?
direct_follow(source_account, target_account, reblogs: reblogs)

View File

@ -9,7 +9,7 @@ class NotifyService < BaseService
return if recipient.user.nil? || blocked?
create_notification!
push_notification! if @notification.browserable?
push_notification!
push_to_conversation! if direct_message?
send_email! if email_enabled?
rescue ActiveRecord::RecordInvalid

View File

@ -14,7 +14,16 @@ class ProcessMentionsService < BaseService
mentions = []
status.text = status.text.gsub(Account::MENTION_RE) do |match|
username, domain = Regexp.last_match(1).split('@')
username, domain = Regexp.last_match(1).split('@')
domain = begin
if TagManager.instance.local_domain?(domain)
nil
else
TagManager.instance.normalize_domain(domain)
end
end
mentioned_account = Account.find_remote(username, domain)
if mention_undeliverable?(mentioned_account)

View File

@ -12,6 +12,8 @@ class ResolveURLService < BaseService
process_local_url
elsif !fetched_resource.nil?
process_url
elsif @on_behalf_of.present?
process_url_from_db
end
end
@ -19,14 +21,24 @@ class ResolveURLService < BaseService
def process_url
if equals_or_includes_any?(type, ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES)
FetchRemoteAccountService.new.call(resource_url, body, protocol)
ActivityPub::FetchRemoteAccountService.new.call(resource_url, prefetched_body: body)
elsif equals_or_includes_any?(type, ActivityPub::Activity::Create::SUPPORTED_TYPES + ActivityPub::Activity::Create::CONVERTED_TYPES)
status = FetchRemoteStatusService.new.call(resource_url, body, protocol)
status = FetchRemoteStatusService.new.call(resource_url, body)
authorize_with @on_behalf_of, status, :show? unless status.nil?
status
end
end
def process_url_from_db
# It may happen that the resource is a private toot, and thus not fetchable,
# but we can return the toot if we already know about it.
status = Status.find_by(uri: @url) || Status.find_by(url: @url)
authorize_with @on_behalf_of, status, :show? unless status.nil?
status
rescue Mastodon::NotPermittedError
nil
end
def fetched_resource
@fetched_resource ||= FetchResourceService.new.call(@url)
end
@ -39,12 +51,8 @@ class ResolveURLService < BaseService
fetched_resource.second[:prefetched_body]
end
def protocol
fetched_resource.third
end
def type
return json_data['type'] if protocol == :activitypub
json_data['type']
end
def json_data

View File

@ -2,7 +2,7 @@
class SearchService < BaseService
def call(query, account, limit, options = {})
@query = query.strip
@query = query&.strip
@account = account
@options = options
@limit = limit.to_i
@ -10,6 +10,8 @@ class SearchService < BaseService
@resolve = options[:resolve] || false
default_results.tap do |results|
next if @query.blank?
if url_query?
results.merge!(url_resource_results) unless url_resource.nil? || (@options[:type].present? && url_resource_symbol != @options[:type].to_sym)
elsif @query.present?

View File

@ -20,7 +20,7 @@ class VoteService < BaseService
ApplicationRecord.transaction do
@choices.each do |choice|
@votes << @poll.votes.create!(account: @account, choice: choice)
@votes << @poll.votes.create!(account: @account, choice: Integer(choice))
end
end
else