Merge tag 'v3.1.1' into instance_only_statuses
This commit is contained in:
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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!
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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?
|
||||
|
||||
@ -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
|
||||
|
||||
Reference in New Issue
Block a user