Merge tag 'v3.1.1' into instance_only_statuses
This commit is contained in:
@ -5,7 +5,7 @@ class ActivityPub::Activity
|
||||
include Redisable
|
||||
|
||||
SUPPORTED_TYPES = %w(Note Question).freeze
|
||||
CONVERTED_TYPES = %w(Image Audio Video Article Page).freeze
|
||||
CONVERTED_TYPES = %w(Image Audio Video Article Page Event).freeze
|
||||
|
||||
def initialize(json, account, **options)
|
||||
@json = json
|
||||
@ -21,7 +21,7 @@ class ActivityPub::Activity
|
||||
class << self
|
||||
def factory(json, account, **options)
|
||||
@json = json
|
||||
klass&.new(json, account, options)
|
||||
klass&.new(json, account, **options)
|
||||
end
|
||||
|
||||
private
|
||||
@ -89,7 +89,7 @@ class ActivityPub::Activity
|
||||
def distribute(status)
|
||||
crawl_links(status)
|
||||
|
||||
notify_about_reblog(status) if reblog_of_local_account?(status)
|
||||
notify_about_reblog(status) if reblog_of_local_account?(status) && !reblog_by_following_group_account?(status)
|
||||
notify_about_mentions(status)
|
||||
|
||||
# Only continue if the status is supposed to have arrived in real-time.
|
||||
@ -105,6 +105,10 @@ class ActivityPub::Activity
|
||||
status.reblog? && status.reblog.account.local?
|
||||
end
|
||||
|
||||
def reblog_by_following_group_account?(status)
|
||||
status.reblog? && status.account.group? && status.reblog.account.following?(status.account)
|
||||
end
|
||||
|
||||
def notify_about_reblog(status)
|
||||
NotifyService.new.call(status.reblog.account, status)
|
||||
end
|
||||
@ -153,6 +157,14 @@ class ActivityPub::Activity
|
||||
fetch_remote_original_status
|
||||
end
|
||||
|
||||
def follow_request_from_object
|
||||
@follow_request ||= FollowRequest.find_by(target_account: @account, uri: object_uri) unless object_uri.nil?
|
||||
end
|
||||
|
||||
def follow_from_object
|
||||
@follow ||= ::Follow.find_by(target_account: @account, uri: object_uri) unless object_uri.nil?
|
||||
end
|
||||
|
||||
def fetch_remote_original_status
|
||||
if object_uri.start_with?('http')
|
||||
return if ActivityPub::TagManager.instance.local_uri?(object_uri)
|
||||
|
@ -2,17 +2,18 @@
|
||||
|
||||
class ActivityPub::Activity::Accept < ActivityPub::Activity
|
||||
def perform
|
||||
return accept_follow_for_relay if relay_follow?
|
||||
return follow_request_from_object.authorize! unless follow_request_from_object.nil?
|
||||
|
||||
case @object['type']
|
||||
when 'Follow'
|
||||
accept_follow
|
||||
accept_embedded_follow
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def accept_follow
|
||||
return accept_follow_for_relay if relay_follow?
|
||||
|
||||
def accept_embedded_follow
|
||||
target_account = account_from_uri(target_uri)
|
||||
|
||||
return if target_account.nil? || !target_account.local?
|
||||
|
@ -25,6 +25,14 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||
|
||||
private
|
||||
|
||||
def audience_to
|
||||
@object['to'] || @json['to']
|
||||
end
|
||||
|
||||
def audience_cc
|
||||
@object['cc'] || @json['cc']
|
||||
end
|
||||
|
||||
def process_status
|
||||
@tags = []
|
||||
@mentions = []
|
||||
@ -75,7 +83,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||
end
|
||||
|
||||
def process_audience
|
||||
(as_array(@object['to']) + as_array(@object['cc'])).uniq.each do |audience|
|
||||
(as_array(audience_to) + as_array(audience_cc)).uniq.each do |audience|
|
||||
next if audience == ActivityPub::TagManager::COLLECTIONS[:public]
|
||||
|
||||
# Unlike with tags, there is no point in resolving accounts we don't already
|
||||
@ -149,7 +157,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||
return if tag['name'].blank?
|
||||
|
||||
Tag.find_or_create_by_names(tag['name']) do |hashtag|
|
||||
@tags << hashtag unless @tags.include?(hashtag)
|
||||
@tags << hashtag unless @tags.include?(hashtag) || !hashtag.valid?
|
||||
end
|
||||
rescue ActiveRecord::RecordInvalid
|
||||
nil
|
||||
@ -159,7 +167,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||
return if tag['href'].blank?
|
||||
|
||||
account = account_from_uri(tag['href'])
|
||||
account = ::FetchRemoteAccountService.new.call(tag['href']) if account.nil?
|
||||
account = ActivityPub::FetchRemoteAccountService.new.call(tag['href']) if account.nil?
|
||||
|
||||
return if account.nil?
|
||||
|
||||
@ -291,11 +299,11 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||
end
|
||||
|
||||
def visibility_from_audience
|
||||
if equals_or_includes?(@object['to'], ActivityPub::TagManager::COLLECTIONS[:public])
|
||||
if equals_or_includes?(audience_to, ActivityPub::TagManager::COLLECTIONS[:public])
|
||||
:public
|
||||
elsif equals_or_includes?(@object['cc'], ActivityPub::TagManager::COLLECTIONS[:public])
|
||||
elsif equals_or_includes?(audience_cc, ActivityPub::TagManager::COLLECTIONS[:public])
|
||||
:unlisted
|
||||
elsif equals_or_includes?(@object['to'], @account.followers_url)
|
||||
elsif equals_or_includes?(audience_to, @account.followers_url)
|
||||
:private
|
||||
else
|
||||
:direct
|
||||
@ -304,7 +312,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||
|
||||
def audience_includes?(account)
|
||||
uri = ActivityPub::TagManager.instance.uri_for(account)
|
||||
equals_or_includes?(@object['to'], uri) || equals_or_includes?(@object['cc'], uri)
|
||||
equals_or_includes?(audience_to, uri) || equals_or_includes?(audience_cc, uri)
|
||||
end
|
||||
|
||||
def replied_to_status
|
||||
@ -415,7 +423,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||
def addresses_local_accounts?
|
||||
return true if @options[:delivered_to_account_id]
|
||||
|
||||
local_usernames = (as_array(@object['to']) + as_array(@object['cc'])).uniq.select { |uri| ActivityPub::TagManager.instance.local_uri?(uri) }.map { |uri| ActivityPub::TagManager.instance.uri_to_local_id(uri, :username) }
|
||||
local_usernames = (as_array(audience_to) + as_array(audience_cc)).uniq.select { |uri| ActivityPub::TagManager.instance.local_uri?(uri) }.map { |uri| ActivityPub::TagManager.instance.uri_to_local_id(uri, :username) }
|
||||
|
||||
return false if local_usernames.empty?
|
||||
|
||||
|
@ -2,17 +2,19 @@
|
||||
|
||||
class ActivityPub::Activity::Reject < ActivityPub::Activity
|
||||
def perform
|
||||
return reject_follow_for_relay if relay_follow?
|
||||
return follow_request_from_object.reject! unless follow_request_from_object.nil?
|
||||
return UnfollowService.new.call(follow_from_object.target_account, @account) unless follow_from_object.nil?
|
||||
|
||||
case @object['type']
|
||||
when 'Follow'
|
||||
reject_follow
|
||||
reject_embedded_follow
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def reject_follow
|
||||
return reject_follow_for_relay if relay_follow?
|
||||
|
||||
def reject_embedded_follow
|
||||
target_account = account_from_uri(target_uri)
|
||||
|
||||
return if target_account.nil? || !target_account.local?
|
||||
|
@ -35,6 +35,7 @@ class ActivityPub::Adapter < ActiveModelSerializers::Adapter::Base
|
||||
def serializable_hash(options = nil)
|
||||
named_contexts = {}
|
||||
context_extensions = {}
|
||||
|
||||
options = serialization_options(options)
|
||||
serialized_hash = serializer.serializable_hash(options.merge(named_contexts: named_contexts, context_extensions: context_extensions))
|
||||
serialized_hash = serialized_hash.select { |k, _| options[:fields].include?(k) } if options[:fields]
|
||||
|
@ -68,10 +68,19 @@ class ActivityPub::TagManager
|
||||
if status.account.silenced?
|
||||
# Only notify followers if the account is locally silenced
|
||||
account_ids = status.active_mentions.pluck(:account_id)
|
||||
to = status.account.followers.where(id: account_ids).map { |account| uri_for(account) }
|
||||
to.concat(FollowRequest.where(target_account_id: status.account_id, account_id: account_ids).map { |request| uri_for(request.account) })
|
||||
to = status.account.followers.where(id: account_ids).each_with_object([]) do |account, result|
|
||||
result << uri_for(account)
|
||||
result << account.followers_url if account.group?
|
||||
end
|
||||
to.concat(FollowRequest.where(target_account_id: status.account_id, account_id: account_ids).each_with_object([]) do |request, result|
|
||||
result << uri_for(request.account)
|
||||
result << request.account.followers_url if request.account.group?
|
||||
end)
|
||||
else
|
||||
status.active_mentions.map { |mention| uri_for(mention.account) }
|
||||
status.active_mentions.each_with_object([]) do |mention, result|
|
||||
result << uri_for(mention.account)
|
||||
result << mention.account.followers_url if mention.account.group?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -97,10 +106,19 @@ class ActivityPub::TagManager
|
||||
if status.account.silenced?
|
||||
# Only notify followers if the account is locally silenced
|
||||
account_ids = status.active_mentions.pluck(:account_id)
|
||||
cc.concat(status.account.followers.where(id: account_ids).map { |account| uri_for(account) })
|
||||
cc.concat(FollowRequest.where(target_account_id: status.account_id, account_id: account_ids).map { |request| uri_for(request.account) })
|
||||
cc.concat(status.account.followers.where(id: account_ids).each_with_object([]) do |account, result|
|
||||
result << uri_for(account)
|
||||
result << account.followers_url if account.group?
|
||||
end)
|
||||
cc.concat(FollowRequest.where(target_account_id: status.account_id, account_id: account_ids).each_with_object([]) do |request, result|
|
||||
result << uri_for(request.account)
|
||||
result << request.account.followers_url if request.account.group?
|
||||
end)
|
||||
else
|
||||
cc.concat(status.active_mentions.map { |mention| uri_for(mention.account) })
|
||||
cc.concat(status.active_mentions.each_with_object([]) do |mention, result|
|
||||
result << uri_for(mention.account)
|
||||
result << mention.account.followers_url if mention.account.group?
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -8,7 +8,7 @@ class EntityCache
|
||||
MAX_EXPIRATION = 7.days.freeze
|
||||
|
||||
def mention(username, domain)
|
||||
Rails.cache.fetch(to_key(:mention, username, domain), expires_in: MAX_EXPIRATION) { Account.select(:username, :domain, :url).find_remote(username, domain) }
|
||||
Rails.cache.fetch(to_key(:mention, username, domain), expires_in: MAX_EXPIRATION) { Account.select(:id, :username, :domain, :url).find_remote(username, domain) }
|
||||
end
|
||||
|
||||
def emoji(shortcodes, domain)
|
||||
|
@ -11,6 +11,10 @@ class FeedManager
|
||||
# Must be <= MAX_ITEMS or the tracking sets will grow forever
|
||||
REBLOG_FALLOFF = 40
|
||||
|
||||
def with_active_accounts(&block)
|
||||
Account.joins(:user).where('users.current_sign_in_at > ?', User::ACTIVE_DURATION.ago).find_each(&block)
|
||||
end
|
||||
|
||||
def key(type, id, subtype = nil)
|
||||
return "feed:#{type}:#{id}" unless subtype
|
||||
|
||||
@ -153,7 +157,7 @@ class FeedManager
|
||||
crutches = build_crutches(account.id, statuses)
|
||||
|
||||
statuses.each do |status|
|
||||
next if filter_from_home?(status, account, crutches)
|
||||
next if filter_from_home?(status, account.id, crutches)
|
||||
|
||||
add_to_feed(:home, account.id, status, aggregate)
|
||||
end
|
||||
|
@ -46,6 +46,8 @@ class Formatter
|
||||
|
||||
def reformat(html)
|
||||
sanitize(html, Sanitize::Config::MASTODON_STRICT)
|
||||
rescue ArgumentError
|
||||
''
|
||||
end
|
||||
|
||||
def plaintext(status)
|
||||
@ -245,13 +247,14 @@ class Formatter
|
||||
end
|
||||
|
||||
standard = Extractor.extract_entities_with_indices(text, options)
|
||||
extra = Extractor.extract_extra_uris_with_indices(text, options)
|
||||
|
||||
Extractor.remove_overlapping_entities(special + standard)
|
||||
Extractor.remove_overlapping_entities(special + standard + extra)
|
||||
end
|
||||
|
||||
def link_to_url(entity, options = {})
|
||||
url = Addressable::URI.parse(entity[:url])
|
||||
html_attrs = { target: '_blank', rel: 'nofollow noopener' }
|
||||
html_attrs = { target: '_blank', rel: 'nofollow noopener noreferrer' }
|
||||
|
||||
html_attrs[:rel] = "me #{html_attrs[:rel]}" if options[:me]
|
||||
|
||||
@ -284,7 +287,7 @@ class Formatter
|
||||
|
||||
def link_html(url)
|
||||
url = Addressable::URI.parse(url).to_s
|
||||
prefix = url.match(/\Ahttps?:\/\/(www\.)?/).to_s
|
||||
prefix = url.match(/\A(https?:\/\/(www\.)?|xmpp:)/).to_s
|
||||
text = url[prefix.length, 30]
|
||||
suffix = url[prefix.length + 30..-1]
|
||||
cutoff = url[prefix.length..-1].length > 30
|
||||
|
@ -15,6 +15,10 @@ class InlineRenderer
|
||||
serializer = REST::NotificationSerializer
|
||||
when :conversation
|
||||
serializer = REST::ConversationSerializer
|
||||
when :announcement
|
||||
serializer = REST::AnnouncementSerializer
|
||||
when :reaction
|
||||
serializer = REST::ReactionSerializer
|
||||
else
|
||||
return
|
||||
end
|
||||
|
@ -44,7 +44,7 @@ class LanguageDetector
|
||||
words = text.scan(RELIABLE_CHARACTERS_RE)
|
||||
|
||||
if words.present?
|
||||
words.reduce(0) { |acc, elem| acc + elem.size }.to_f / text.size.to_f > 0.3
|
||||
words.reduce(0) { |acc, elem| acc + elem.size }.to_f / text.size > 0.3
|
||||
else
|
||||
false
|
||||
end
|
||||
|
@ -59,7 +59,7 @@ class Request
|
||||
begin
|
||||
response = http_client.public_send(@verb, @url.to_s, @options.merge(headers: headers))
|
||||
rescue => e
|
||||
raise e.class, "#{e.message} on #{@url}", e.backtrace
|
||||
raise e.class, "#{e.message} on #{@url}", e.backtrace[0]
|
||||
end
|
||||
|
||||
begin
|
||||
@ -73,6 +73,8 @@ class Request
|
||||
response.body_with_limit if http_client.persistent?
|
||||
|
||||
yield response if block_given?
|
||||
rescue => e
|
||||
raise e.class, e.message, e.backtrace[0]
|
||||
ensure
|
||||
http_client.close unless http_client.persistent?
|
||||
end
|
||||
@ -94,7 +96,7 @@ class Request
|
||||
end
|
||||
|
||||
def http_client
|
||||
HTTP.use(:auto_inflate).timeout(:per_operation, TIMEOUT.dup).follow(max_hops: 2)
|
||||
HTTP.use(:auto_inflate).timeout(TIMEOUT.dup).follow(max_hops: 2)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -2,7 +2,23 @@
|
||||
|
||||
class Sanitize
|
||||
module Config
|
||||
HTTP_PROTOCOLS ||= ['http', 'https', 'dat', 'dweb', 'ipfs', 'ipns', 'ssb', 'gopher', :relative].freeze
|
||||
HTTP_PROTOCOLS = %w(
|
||||
http
|
||||
https
|
||||
).freeze
|
||||
|
||||
LINK_PROTOCOLS = %w(
|
||||
http
|
||||
https
|
||||
dat
|
||||
dweb
|
||||
ipfs
|
||||
ipns
|
||||
ssb
|
||||
gopher
|
||||
xmpp
|
||||
magnet
|
||||
).freeze
|
||||
|
||||
CLASS_WHITELIST_TRANSFORMER = lambda do |env|
|
||||
node = env[:node]
|
||||
@ -19,19 +35,37 @@ class Sanitize
|
||||
node['class'] = class_list.join(' ')
|
||||
end
|
||||
|
||||
UNSUPPORTED_HREF_TRANSFORMER = lambda do |env|
|
||||
return unless env[:node_name] == 'a'
|
||||
|
||||
current_node = env[:node]
|
||||
|
||||
scheme = begin
|
||||
if current_node['href'] =~ Sanitize::REGEX_PROTOCOL
|
||||
Regexp.last_match(1).downcase
|
||||
else
|
||||
:relative
|
||||
end
|
||||
end
|
||||
|
||||
current_node.replace(current_node.text) unless LINK_PROTOCOLS.include?(scheme)
|
||||
end
|
||||
|
||||
UNSUPPORTED_ELEMENTS_TRANSFORMER = lambda do |env|
|
||||
return unless %w(h1 h2 h3 h4 h5 h6 blockquote pre ul ol li).include?(env[:node_name])
|
||||
|
||||
current_node = env[:node]
|
||||
|
||||
case env[:node_name]
|
||||
when 'li'
|
||||
env[:node].traverse do |node|
|
||||
current_node.traverse do |node|
|
||||
next unless %w(p ul ol li).include?(node.name)
|
||||
|
||||
node.add_next_sibling('<br>') if node.next_sibling
|
||||
node.replace(node.children) unless node.text?
|
||||
end
|
||||
else
|
||||
env[:node].name = 'p'
|
||||
current_node.name = 'p'
|
||||
end
|
||||
end
|
||||
|
||||
@ -45,18 +79,17 @@ class Sanitize
|
||||
|
||||
add_attributes: {
|
||||
'a' => {
|
||||
'rel' => 'nofollow noopener',
|
||||
'rel' => 'nofollow noopener noreferrer',
|
||||
'target' => '_blank',
|
||||
},
|
||||
},
|
||||
|
||||
protocols: {
|
||||
'a' => { 'href' => HTTP_PROTOCOLS },
|
||||
},
|
||||
protocols: {},
|
||||
|
||||
transformers: [
|
||||
CLASS_WHITELIST_TRANSFORMER,
|
||||
UNSUPPORTED_ELEMENTS_TRANSFORMER,
|
||||
UNSUPPORTED_HREF_TRANSFORMER,
|
||||
]
|
||||
)
|
||||
|
||||
|
@ -78,7 +78,7 @@ class SearchQueryTransformer < Parslet::Transform
|
||||
elsif clause[:shortcode]
|
||||
TermClause.new(prefix, operator, ":#{clause[:term]}:")
|
||||
elsif clause[:phrase]
|
||||
PhraseClause.new(prefix, operator, clause[:phrase].map { |p| p[:term].to_s }.join(' '))
|
||||
PhraseClause.new(prefix, operator, clause[:phrase].is_a?(Array) ? clause[:phrase].map { |p| p[:term].to_s }.join(' ') : clause[:phrase].to_s)
|
||||
else
|
||||
raise "Unexpected clause type: #{clause}"
|
||||
end
|
||||
|
@ -143,7 +143,7 @@ class SpamCheck
|
||||
end
|
||||
|
||||
def trusted?
|
||||
@account.trust_level > Account::TRUST_LEVELS[:untrusted]
|
||||
@account.trust_level > Account::TRUST_LEVELS[:untrusted] || (@account.local? && @account.user_staff?)
|
||||
end
|
||||
|
||||
def no_unsolicited_mentions?
|
||||
|
@ -38,6 +38,7 @@ class UserSettingsDecorator
|
||||
user.settings['use_blurhash'] = use_blurhash_preference if change?('setting_use_blurhash')
|
||||
user.settings['use_pending_items'] = use_pending_items_preference if change?('setting_use_pending_items')
|
||||
user.settings['trends'] = trends_preference if change?('setting_trends')
|
||||
user.settings['crop_images'] = crop_images_preference if change?('setting_crop_images')
|
||||
end
|
||||
|
||||
def merged_notification_emails
|
||||
@ -132,6 +133,10 @@ class UserSettingsDecorator
|
||||
boolean_cast_setting 'setting_trends'
|
||||
end
|
||||
|
||||
def crop_images_preference
|
||||
boolean_cast_setting 'setting_crop_images'
|
||||
end
|
||||
|
||||
def boolean_cast_setting(key)
|
||||
ActiveModel::Type::Boolean.new.cast(settings[key])
|
||||
end
|
||||
|
Reference in New Issue
Block a user