Merge remote-tracking branch 'parent/stable-4.2' into kb-draft-5.14-lts
This commit is contained in:
commit
1d42b6b82f
59 changed files with 810 additions and 489 deletions
|
@ -25,6 +25,6 @@ class Api::V1::Accounts::NotesController < Api::BaseController
|
|||
end
|
||||
|
||||
def relationships_presenter
|
||||
AccountRelationshipsPresenter.new([@account.id], current_user.account_id)
|
||||
AccountRelationshipsPresenter.new([@account], current_user.account_id)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -25,6 +25,6 @@ class Api::V1::Accounts::PinsController < Api::BaseController
|
|||
end
|
||||
|
||||
def relationships_presenter
|
||||
AccountRelationshipsPresenter.new([@account.id], current_user.account_id)
|
||||
AccountRelationshipsPresenter.new([@account], current_user.account_id)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,11 +5,10 @@ class Api::V1::Accounts::RelationshipsController < Api::BaseController
|
|||
before_action :require_user!
|
||||
|
||||
def index
|
||||
accounts = Account.without_suspended.where(id: account_ids).select('id')
|
||||
@accounts = Account.without_suspended.where(id: account_ids).select(:id, :domain).to_a
|
||||
# .where doesn't guarantee that our results are in the same order
|
||||
# we requested them, so return the "right" order to the requestor.
|
||||
@accounts = accounts.index_by(&:id).values_at(*account_ids).compact
|
||||
render json: @accounts, each_serializer: REST::RelationshipSerializer, relationships: relationships
|
||||
render json: @accounts.index_by(&:id).values_at(*account_ids).compact, each_serializer: REST::RelationshipSerializer, relationships: relationships
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -86,7 +86,7 @@ class Api::V1::AccountsController < Api::BaseController
|
|||
end
|
||||
|
||||
def relationships(**options)
|
||||
AccountRelationshipsPresenter.new([@account.id], current_user.account_id, **options)
|
||||
AccountRelationshipsPresenter.new([@account], current_user.account_id, **options)
|
||||
end
|
||||
|
||||
def account_params
|
||||
|
|
|
@ -25,11 +25,11 @@ class Api::V1::FollowRequestsController < Api::BaseController
|
|||
private
|
||||
|
||||
def account
|
||||
Account.find(params[:id])
|
||||
@account ||= Account.find(params[:id])
|
||||
end
|
||||
|
||||
def relationships(**options)
|
||||
AccountRelationshipsPresenter.new([params[:id]], current_user.account_id, **options)
|
||||
AccountRelationshipsPresenter.new([account], current_user.account_id, **options)
|
||||
end
|
||||
|
||||
def load_accounts
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
class Api::V1::StreamingController < Api::BaseController
|
||||
def index
|
||||
if Rails.configuration.x.streaming_api_base_url == request.host
|
||||
if same_host?
|
||||
not_found
|
||||
else
|
||||
redirect_to streaming_api_url, status: 301, allow_other_host: true
|
||||
|
@ -11,9 +11,16 @@ class Api::V1::StreamingController < Api::BaseController
|
|||
|
||||
private
|
||||
|
||||
def same_host?
|
||||
base_url = Addressable::URI.parse(Rails.configuration.x.streaming_api_base_url)
|
||||
request.host == base_url.host && request.port == (base_url.port || 80)
|
||||
end
|
||||
|
||||
def streaming_api_url
|
||||
Addressable::URI.parse(request.url).tap do |uri|
|
||||
uri.host = Addressable::URI.parse(Rails.configuration.x.streaming_api_base_url).host
|
||||
base_url = Addressable::URI.parse(Rails.configuration.x.streaming_api_base_url)
|
||||
uri.host = base_url.host
|
||||
uri.port = base_url.port
|
||||
end.to_s
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Auth::SessionsController < Devise::SessionsController
|
||||
include Redisable
|
||||
|
||||
MAX_2FA_ATTEMPTS_PER_HOUR = 10
|
||||
|
||||
layout 'auth'
|
||||
|
||||
skip_before_action :require_no_authentication, only: [:create]
|
||||
|
@ -134,9 +138,23 @@ class Auth::SessionsController < Devise::SessionsController
|
|||
session.delete(:attempt_user_updated_at)
|
||||
end
|
||||
|
||||
def clear_2fa_attempt_from_user(user)
|
||||
redis.del(second_factor_attempts_key(user))
|
||||
end
|
||||
|
||||
def check_second_factor_rate_limits(user)
|
||||
attempts, = redis.multi do |multi|
|
||||
multi.incr(second_factor_attempts_key(user))
|
||||
multi.expire(second_factor_attempts_key(user), 1.hour)
|
||||
end
|
||||
|
||||
attempts >= MAX_2FA_ATTEMPTS_PER_HOUR
|
||||
end
|
||||
|
||||
def on_authentication_success(user, security_measure)
|
||||
@on_authentication_success_called = true
|
||||
|
||||
clear_2fa_attempt_from_user(user)
|
||||
clear_attempt_from_session
|
||||
|
||||
user.update_sign_in!(new_sign_in: true)
|
||||
|
@ -168,4 +186,8 @@ class Auth::SessionsController < Devise::SessionsController
|
|||
user_agent: request.user_agent
|
||||
)
|
||||
end
|
||||
|
||||
def second_factor_attempts_key(user)
|
||||
"2fa_auth_attempts:#{user.id}:#{Time.now.utc.hour}"
|
||||
end
|
||||
end
|
||||
|
|
|
@ -91,14 +91,23 @@ module SignatureVerification
|
|||
raise SignatureVerificationError, "Public key not found for key #{signature_params['keyId']}" if actor.nil?
|
||||
|
||||
signature = Base64.decode64(signature_params['signature'])
|
||||
compare_signed_string = build_signed_string
|
||||
compare_signed_string = build_signed_string(include_query_string: true)
|
||||
|
||||
return actor unless verify_signature(actor, signature, compare_signed_string).nil?
|
||||
|
||||
# Compatibility quirk with older Mastodon versions
|
||||
compare_signed_string = build_signed_string(include_query_string: false)
|
||||
return actor unless verify_signature(actor, signature, compare_signed_string).nil?
|
||||
|
||||
actor = stoplight_wrap_request { actor_refresh_key!(actor) }
|
||||
|
||||
raise SignatureVerificationError, "Could not refresh public key #{signature_params['keyId']}" if actor.nil?
|
||||
|
||||
compare_signed_string = build_signed_string(include_query_string: true)
|
||||
return actor unless verify_signature(actor, signature, compare_signed_string).nil?
|
||||
|
||||
# Compatibility quirk with older Mastodon versions
|
||||
compare_signed_string = build_signed_string(include_query_string: false)
|
||||
return actor unless verify_signature(actor, signature, compare_signed_string).nil?
|
||||
|
||||
fail_with! "Verification failed for #{actor.to_log_human_identifier} #{actor.uri} using rsa-sha256 (RSASSA-PKCS1-v1_5 with SHA-256)", signed_string: compare_signed_string, signature: signature_params['signature']
|
||||
|
@ -180,11 +189,18 @@ module SignatureVerification
|
|||
nil
|
||||
end
|
||||
|
||||
def build_signed_string
|
||||
def build_signed_string(include_query_string: true)
|
||||
signed_headers.map do |signed_header|
|
||||
case signed_header
|
||||
when Request::REQUEST_TARGET
|
||||
"#{Request::REQUEST_TARGET}: #{request.method.downcase} #{request.path}"
|
||||
if include_query_string
|
||||
"#{Request::REQUEST_TARGET}: #{request.method.downcase} #{request.original_fullpath}"
|
||||
else
|
||||
# Current versions of Mastodon incorrectly omit the query string from the (request-target) pseudo-header.
|
||||
# Therefore, temporarily support such incorrect signatures for compatibility.
|
||||
# TODO: remove eventually some time after release of the fixed version
|
||||
"#{Request::REQUEST_TARGET}: #{request.method.downcase} #{request.path}"
|
||||
end
|
||||
when '(created)'
|
||||
raise SignatureVerificationError, 'Invalid pseudo-header (created) for rsa-sha256' unless signature_algorithm == 'hs2019'
|
||||
raise SignatureVerificationError, 'Pseudo-header (created) used but corresponding argument missing' if signature_params['created'].blank?
|
||||
|
|
|
@ -65,6 +65,11 @@ module TwoFactorAuthenticationConcern
|
|||
end
|
||||
|
||||
def authenticate_with_two_factor_via_otp(user)
|
||||
if check_second_factor_rate_limits(user)
|
||||
flash.now[:alert] = I18n.t('users.rate_limited')
|
||||
return prompt_for_two_factor(user)
|
||||
end
|
||||
|
||||
if valid_otp_attempt?(user)
|
||||
on_authentication_success(user, :otp)
|
||||
else
|
||||
|
|
|
@ -33,7 +33,7 @@ class RelationshipsController < ApplicationController
|
|||
end
|
||||
|
||||
def set_relationships
|
||||
@relationships = AccountRelationshipsPresenter.new(@accounts.pluck(:id), current_user.account_id)
|
||||
@relationships = AccountRelationshipsPresenter.new(@accounts, current_user.account_id)
|
||||
end
|
||||
|
||||
def form_account_batch_params
|
||||
|
|
|
@ -155,7 +155,7 @@ module JsonLdHelper
|
|||
end
|
||||
end
|
||||
|
||||
def fetch_resource(uri, id, on_behalf_of = nil)
|
||||
def fetch_resource(uri, id, on_behalf_of = nil, request_options: {})
|
||||
unless id
|
||||
json = fetch_resource_without_id_validation(uri, on_behalf_of)
|
||||
|
||||
|
@ -164,14 +164,14 @@ module JsonLdHelper
|
|||
uri = json['id']
|
||||
end
|
||||
|
||||
json = fetch_resource_without_id_validation(uri, on_behalf_of)
|
||||
json = fetch_resource_without_id_validation(uri, on_behalf_of, request_options: request_options)
|
||||
json.present? && json['id'] == uri ? json : nil
|
||||
end
|
||||
|
||||
def fetch_resource_without_id_validation(uri, on_behalf_of = nil, raise_on_temporary_error = false)
|
||||
def fetch_resource_without_id_validation(uri, on_behalf_of = nil, raise_on_temporary_error = false, request_options: {})
|
||||
on_behalf_of ||= Account.representative
|
||||
|
||||
build_request(uri, on_behalf_of).perform do |response|
|
||||
build_request(uri, on_behalf_of, options: request_options).perform do |response|
|
||||
raise Mastodon::UnexpectedResponseError, response unless response_successful?(response) || response_error_unsalvageable?(response) || !raise_on_temporary_error
|
||||
|
||||
body_to_json(response.body_with_limit) if response.code == 200
|
||||
|
@ -204,8 +204,8 @@ module JsonLdHelper
|
|||
response.code == 501 || ((400...500).cover?(response.code) && ![401, 408, 429].include?(response.code))
|
||||
end
|
||||
|
||||
def build_request(uri, on_behalf_of = nil)
|
||||
Request.new(:get, uri).tap do |request|
|
||||
def build_request(uri, on_behalf_of = nil, options: {})
|
||||
Request.new(:get, uri, **options).tap do |request|
|
||||
request.on_behalf_of(on_behalf_of) if on_behalf_of
|
||||
request.add_headers('Accept' => 'application/activity+json, application/ld+json')
|
||||
end
|
||||
|
|
|
@ -4,7 +4,7 @@ import { PureComponent } from 'react';
|
|||
const iconStyle = {
|
||||
height: null,
|
||||
lineHeight: '27px',
|
||||
width: `${18 * 1.28571429}px`,
|
||||
minWidth: `${18 * 1.28571429}px`,
|
||||
};
|
||||
|
||||
export default class TextIconButton extends PureComponent {
|
||||
|
|
|
@ -45,24 +45,20 @@ class Statuses extends PureComponent {
|
|||
const emptyMessage = <FormattedMessage id='empty_column.explore_statuses' defaultMessage='Nothing is trending right now. Check back later!' />;
|
||||
|
||||
return (
|
||||
<>
|
||||
<DismissableBanner id='explore/statuses'>
|
||||
<FormattedMessage id='dismissable_banner.explore_statuses' defaultMessage='These are posts from across the social web that are gaining traction today. Newer posts with more boosts and favorites are ranked higher.' />
|
||||
</DismissableBanner>
|
||||
|
||||
<StatusList
|
||||
trackScroll
|
||||
timelineId='explore'
|
||||
statusIds={statusIds}
|
||||
scrollKey='explore-statuses'
|
||||
hasMore={hasMore}
|
||||
isLoading={isLoading}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
emptyMessage={emptyMessage}
|
||||
bindToDocument={!multiColumn}
|
||||
withCounters
|
||||
/>
|
||||
</>
|
||||
<StatusList
|
||||
trackScroll
|
||||
prepend={<DismissableBanner id='explore/statuses'><FormattedMessage id='dismissable_banner.explore_statuses' defaultMessage='These are posts from across the social web that are gaining traction today. Newer posts with more boosts and favorites are ranked higher.' /></DismissableBanner>}
|
||||
alwaysPrepend
|
||||
timelineId='explore'
|
||||
statusIds={statusIds}
|
||||
scrollKey='explore-statuses'
|
||||
hasMore={hasMore}
|
||||
isLoading={isLoading}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
emptyMessage={emptyMessage}
|
||||
bindToDocument={!multiColumn}
|
||||
withCounters
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -207,7 +207,7 @@ class ListTimeline extends PureComponent {
|
|||
</div>
|
||||
|
||||
<div className='setting-toggle'>
|
||||
<Toggle id={`list-${id}-exclusive`} defaultChecked={isExclusive} onChange={this.onExclusiveToggle} />
|
||||
<Toggle id={`list-${id}-exclusive`} checked={isExclusive} onChange={this.onExclusiveToggle} />
|
||||
<label htmlFor={`list-${id}-exclusive`} className='setting-toggle__label'>
|
||||
<FormattedMessage id='lists.exclusive' defaultMessage='Hide these posts from home or STL' />
|
||||
</label>
|
||||
|
|
|
@ -284,6 +284,7 @@
|
|||
font-size: 11px;
|
||||
padding: 0 3px;
|
||||
line-height: 27px;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover,
|
||||
&:active,
|
||||
|
@ -4690,11 +4691,6 @@ a.status-card {
|
|||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
@supports (display: grid) {
|
||||
// hack to fix Chrome <57
|
||||
contain: strict;
|
||||
}
|
||||
|
||||
& > span {
|
||||
max-width: 500px;
|
||||
}
|
||||
|
|
|
@ -42,13 +42,13 @@ class InlineRenderer
|
|||
private
|
||||
|
||||
def preload_associations_for_status
|
||||
ActiveRecord::Associations::Preloader.new(records: @object, associations: {
|
||||
ActiveRecord::Associations::Preloader.new(records: [@object], associations: {
|
||||
active_mentions: :account,
|
||||
|
||||
reblog: {
|
||||
active_mentions: :account,
|
||||
},
|
||||
})
|
||||
}).call
|
||||
end
|
||||
|
||||
def current_user
|
||||
|
|
|
@ -37,6 +37,7 @@ class LinkDetailsExtractor
|
|||
|
||||
def language
|
||||
lang = json['inLanguage']
|
||||
lang = lang.first if lang.is_a?(Array)
|
||||
lang.is_a?(Hash) ? (lang['alternateName'] || lang['name']) : lang
|
||||
end
|
||||
|
||||
|
|
|
@ -77,6 +77,7 @@ class Request
|
|||
@url = Addressable::URI.parse(url).normalize
|
||||
@http_client = options.delete(:http_client)
|
||||
@allow_local = options.delete(:allow_local)
|
||||
@full_path = options.delete(:with_query_string)
|
||||
@options = options.merge(socket_class: use_proxy? || @allow_local ? ProxySocket : Socket)
|
||||
@options = @options.merge(timeout_class: PerOperationWithDeadline, timeout_options: TIMEOUT)
|
||||
@options = @options.merge(proxy_url) if use_proxy?
|
||||
|
@ -146,7 +147,7 @@ class Request
|
|||
private
|
||||
|
||||
def set_common_headers!
|
||||
@headers[REQUEST_TARGET] = "#{@verb} #{@url.path}"
|
||||
@headers[REQUEST_TARGET] = request_target
|
||||
@headers['User-Agent'] = Mastodon::Version.user_agent
|
||||
@headers['Host'] = @url.host
|
||||
@headers['Date'] = Time.now.utc.httpdate
|
||||
|
@ -157,6 +158,14 @@ class Request
|
|||
@headers['Digest'] = "SHA-256=#{Digest::SHA256.base64digest(@options[:body])}"
|
||||
end
|
||||
|
||||
def request_target
|
||||
if @url.query.nil? || !@full_path
|
||||
"#{@verb} #{@url.path}"
|
||||
else
|
||||
"#{@verb} #{@url.path}?#{@url.query}"
|
||||
end
|
||||
end
|
||||
|
||||
def signature
|
||||
algorithm = 'rsa-sha256'
|
||||
signature = Base64.strict_encode64(@keypair.sign(OpenSSL::Digest.new('SHA256'), signed_string))
|
||||
|
|
|
@ -30,40 +30,34 @@ class StatusReachFinder
|
|||
private
|
||||
|
||||
def reached_account_inboxes
|
||||
Account.where(id: reached_account_ids).where.not(domain: banned_domains).inboxes
|
||||
end
|
||||
|
||||
def reached_account_inboxes_for_misskey
|
||||
Account.where(id: reached_account_ids).where(domain: banned_domains_for_misskey).inboxes
|
||||
end
|
||||
|
||||
def reached_account_ids
|
||||
# When the status is a reblog, there are no interactions with it
|
||||
# directly, we assume all interactions are with the original one
|
||||
|
||||
if @status.reblog?
|
||||
[]
|
||||
[reblog_of_account_id]
|
||||
elsif @status.limited_visibility?
|
||||
Account.where(id: mentioned_account_ids).where.not(domain: banned_domains).inboxes
|
||||
[mentioned_account_ids]
|
||||
else
|
||||
Account.where(id: reached_account_ids).where.not(domain: banned_domains).inboxes
|
||||
end
|
||||
end
|
||||
|
||||
def reached_account_inboxes_for_misskey
|
||||
if @status.reblog?
|
||||
[]
|
||||
elsif @status.limited_visibility?
|
||||
Account.where(id: mentioned_account_ids).where(domain: banned_domains_for_misskey).inboxes
|
||||
else
|
||||
Account.where(id: reached_account_ids).where(domain: banned_domains_for_misskey).inboxes
|
||||
end
|
||||
end
|
||||
|
||||
def reached_account_ids
|
||||
[
|
||||
replied_to_account_id,
|
||||
reblog_of_account_id,
|
||||
mentioned_account_ids,
|
||||
reblogs_account_ids,
|
||||
favourites_account_ids,
|
||||
replies_account_ids,
|
||||
].tap do |arr|
|
||||
arr.flatten!
|
||||
arr.compact!
|
||||
arr.uniq!
|
||||
[
|
||||
replied_to_account_id,
|
||||
reblog_of_account_id,
|
||||
mentioned_account_ids,
|
||||
reblogs_account_ids,
|
||||
favourites_account_ids,
|
||||
replies_account_ids,
|
||||
].tap do |arr|
|
||||
arr.flatten!
|
||||
arr.compact!
|
||||
arr.uniq!
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -18,16 +18,12 @@ class AccountDomainBlock < ApplicationRecord
|
|||
belongs_to :account
|
||||
validates :domain, presence: true, uniqueness: { scope: :account_id }, domain: true
|
||||
|
||||
after_commit :remove_blocking_cache
|
||||
after_commit :remove_relationship_cache
|
||||
after_commit :invalidate_domain_blocking_cache
|
||||
|
||||
private
|
||||
|
||||
def remove_blocking_cache
|
||||
def invalidate_domain_blocking_cache
|
||||
Rails.cache.delete("exclude_domains_for:#{account_id}")
|
||||
end
|
||||
|
||||
def remove_relationship_cache
|
||||
Rails.cache.delete_matched("relationship:#{account_id}:*")
|
||||
Rails.cache.delete(['exclude_domains', account_id, domain])
|
||||
end
|
||||
end
|
||||
|
|
|
@ -78,9 +78,9 @@ class Announcement < ApplicationRecord
|
|||
else
|
||||
scope.select("name, custom_emoji_id, count(*) as count, exists(select 1 from announcement_reactions r where r.account_id = #{account.id} and r.announcement_id = announcement_reactions.announcement_id and r.name = announcement_reactions.name) as me")
|
||||
end
|
||||
end
|
||||
end.to_a
|
||||
|
||||
ActiveRecord::Associations::Preloader.new(records: records, associations: :custom_emoji)
|
||||
ActiveRecord::Associations::Preloader.new(records: records, associations: :custom_emoji).call
|
||||
records
|
||||
end
|
||||
|
||||
|
|
|
@ -60,12 +60,6 @@ module AccountInteractions
|
|||
end
|
||||
end
|
||||
|
||||
def domain_blocking_map(target_account_ids, account_id)
|
||||
accounts_map = Account.where(id: target_account_ids).select('id, domain').each_with_object({}) { |a, h| h[a.id] = a.domain }
|
||||
blocked_domains = domain_blocking_map_by_domain(accounts_map.values.compact, account_id)
|
||||
accounts_map.reduce({}) { |h, (id, domain)| h.merge(id => blocked_domains[domain]) }
|
||||
end
|
||||
|
||||
def domain_blocking_map_by_domain(target_domains, account_id)
|
||||
follow_mapping(AccountDomainBlock.where(account_id: account_id, domain: target_domains), :domain)
|
||||
end
|
||||
|
|
|
@ -145,7 +145,7 @@ module AccountSearch
|
|||
tsquery = generate_query_for_search(terms)
|
||||
|
||||
find_by_sql([BASIC_SEARCH_SQL, { limit: limit, offset: offset, tsquery: tsquery }]).tap do |records|
|
||||
ActiveRecord::Associations::Preloader.new(records: records, associations: :account_stat)
|
||||
ActiveRecord::Associations::Preloader.new(records: records, associations: [:account_stat, { user: :role }]).call
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -158,7 +158,7 @@ module AccountSearch
|
|||
end
|
||||
|
||||
find_by_sql([sql_template, { id: account.id, limit: limit, offset: offset, tsquery: tsquery }]).tap do |records|
|
||||
ActiveRecord::Associations::Preloader.new(records: records, associations: :account_stat)
|
||||
ActiveRecord::Associations::Preloader.new(records: records, associations: [:account_stat, { user: :role }]).call
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ module RelationshipCacheable
|
|||
private
|
||||
|
||||
def remove_relationship_cache
|
||||
Rails.cache.delete("relationship:#{account_id}:#{target_account_id}")
|
||||
Rails.cache.delete("relationship:#{target_account_id}:#{account_id}")
|
||||
Rails.cache.delete(['relationship', account_id, target_account_id])
|
||||
Rails.cache.delete(['relationship', target_account_id, account_id])
|
||||
end
|
||||
end
|
||||
|
|
|
@ -128,7 +128,7 @@ class Notification < ApplicationRecord
|
|||
|
||||
# Instead of using the usual `includes`, manually preload each type.
|
||||
# If polymorphic associations are loaded with the usual `includes`, other types of associations will be loaded more.
|
||||
ActiveRecord::Associations::Preloader.new(records: grouped_notifications, associations: associations)
|
||||
ActiveRecord::Associations::Preloader.new(records: grouped_notifications, associations: associations).call
|
||||
end
|
||||
|
||||
unique_target_statuses = notifications.filter_map(&:target_status).uniq
|
||||
|
|
|
@ -98,11 +98,9 @@ class User < ApplicationRecord
|
|||
accepts_nested_attributes_for :invite_request, reject_if: ->(attributes) { attributes['text'].blank? && !Setting.require_invite_text }
|
||||
validates :invite_request, presence: true, on: :create, if: :invite_text_required?
|
||||
|
||||
validates :locale, inclusion: I18n.available_locales.map(&:to_s), if: :locale?
|
||||
validates_with BlacklistedEmailValidator, if: -> { ENV['EMAIL_DOMAIN_LISTS_APPLY_AFTER_CONFIRMATION'] == 'true' || !confirmed? }
|
||||
validates_with EmailMxValidator, if: :validate_email_dns?
|
||||
validates :agreement, acceptance: { allow_nil: false, accept: [true, 'true', '1'] }, on: :create
|
||||
validates :time_zone, inclusion: { in: ActiveSupport::TimeZone.all.map { |tz| tz.tzinfo.name } }, allow_blank: true
|
||||
|
||||
# Honeypot/anti-spam fields
|
||||
attr_accessor :registration_form_time, :website, :confirm_password
|
||||
|
@ -126,6 +124,8 @@ class User < ApplicationRecord
|
|||
|
||||
before_validation :sanitize_languages
|
||||
before_validation :sanitize_role
|
||||
before_validation :sanitize_time_zone
|
||||
before_validation :sanitize_locale
|
||||
before_create :set_approved
|
||||
after_commit :send_pending_devise_notifications
|
||||
after_create_commit :trigger_webhooks
|
||||
|
@ -453,9 +453,15 @@ class User < ApplicationRecord
|
|||
end
|
||||
|
||||
def sanitize_role
|
||||
return if role.nil?
|
||||
self.role = nil if role.present? && role.everyone?
|
||||
end
|
||||
|
||||
self.role = nil if role.everyone?
|
||||
def sanitize_time_zone
|
||||
self.time_zone = nil if time_zone.present? && ActiveSupport::TimeZone[time_zone].nil?
|
||||
end
|
||||
|
||||
def sanitize_locale
|
||||
self.locale = nil if locale.present? && I18n.available_locales.exclude?(locale.to_sym)
|
||||
end
|
||||
|
||||
def prepare_new_user!
|
||||
|
|
|
@ -5,8 +5,9 @@ class AccountRelationshipsPresenter
|
|||
:muting, :requested, :requested_by, :domain_blocking,
|
||||
:endorsed, :account_note
|
||||
|
||||
def initialize(account_ids, current_account_id, **options)
|
||||
@account_ids = account_ids.map { |a| a.is_a?(Account) ? a.id : a.to_i }
|
||||
def initialize(accounts, current_account_id, **options)
|
||||
@accounts = accounts.to_a
|
||||
@account_ids = @accounts.pluck(:id)
|
||||
@current_account_id = current_account_id
|
||||
|
||||
@following = cached[:following].merge(Account.following_map(@uncached_account_ids, @current_account_id))
|
||||
|
@ -16,10 +17,11 @@ class AccountRelationshipsPresenter
|
|||
@muting = cached[:muting].merge(Account.muting_map(@uncached_account_ids, @current_account_id))
|
||||
@requested = cached[:requested].merge(Account.requested_map(@uncached_account_ids, @current_account_id))
|
||||
@requested_by = cached[:requested_by].merge(Account.requested_by_map(@uncached_account_ids, @current_account_id))
|
||||
@domain_blocking = cached[:domain_blocking].merge(Account.domain_blocking_map(@uncached_account_ids, @current_account_id))
|
||||
@endorsed = cached[:endorsed].merge(Account.endorsed_map(@uncached_account_ids, @current_account_id))
|
||||
@account_note = cached[:account_note].merge(Account.account_note_map(@uncached_account_ids, @current_account_id))
|
||||
|
||||
@domain_blocking = domain_blocking_map
|
||||
|
||||
cache_uncached!
|
||||
|
||||
@following.merge!(options[:following_map] || {})
|
||||
|
@ -36,6 +38,31 @@ class AccountRelationshipsPresenter
|
|||
|
||||
private
|
||||
|
||||
def domain_blocking_map
|
||||
target_domains = @accounts.pluck(:domain).compact.uniq
|
||||
blocks_by_domain = {}
|
||||
|
||||
# Fetch from cache
|
||||
cache_keys = target_domains.map { |domain| domain_cache_key(domain) }
|
||||
Rails.cache.read_multi(*cache_keys).each do |key, blocking|
|
||||
blocks_by_domain[key.last] = blocking
|
||||
end
|
||||
|
||||
uncached_domains = target_domains - blocks_by_domain.keys
|
||||
|
||||
# Read uncached values from database
|
||||
AccountDomainBlock.where(account_id: @current_account_id, domain: uncached_domains).pluck(:domain).each do |domain|
|
||||
blocks_by_domain[domain] = true
|
||||
end
|
||||
|
||||
# Write database reads to cache
|
||||
to_cache = uncached_domains.to_h { |domain| [domain_cache_key(domain), blocks_by_domain[domain]] }
|
||||
Rails.cache.write_multi(to_cache, expires_in: 1.day)
|
||||
|
||||
# Return formatted value
|
||||
@accounts.each_with_object({}) { |account, h| h[account.id] = blocks_by_domain[account.domain] }
|
||||
end
|
||||
|
||||
def cached
|
||||
return @cached if defined?(@cached)
|
||||
|
||||
|
@ -47,28 +74,23 @@ class AccountRelationshipsPresenter
|
|||
muting: {},
|
||||
requested: {},
|
||||
requested_by: {},
|
||||
domain_blocking: {},
|
||||
endorsed: {},
|
||||
account_note: {},
|
||||
}
|
||||
|
||||
@uncached_account_ids = []
|
||||
@uncached_account_ids = @account_ids.uniq
|
||||
|
||||
@account_ids.each do |account_id|
|
||||
maps_for_account = Rails.cache.read("relationship:#{@current_account_id}:#{account_id}")
|
||||
|
||||
if maps_for_account.is_a?(Hash)
|
||||
@cached.deep_merge!(maps_for_account)
|
||||
else
|
||||
@uncached_account_ids << account_id
|
||||
end
|
||||
cache_ids = @account_ids.map { |account_id| relationship_cache_key(account_id) }
|
||||
Rails.cache.read_multi(*cache_ids).each do |key, maps_for_account|
|
||||
@cached.deep_merge!(maps_for_account)
|
||||
@uncached_account_ids.delete(key.last)
|
||||
end
|
||||
|
||||
@cached
|
||||
end
|
||||
|
||||
def cache_uncached!
|
||||
@uncached_account_ids.each do |account_id|
|
||||
to_cache = @uncached_account_ids.to_h do |account_id|
|
||||
maps_for_account = {
|
||||
following: { account_id => following[account_id] },
|
||||
followed_by: { account_id => followed_by[account_id] },
|
||||
|
@ -77,12 +99,21 @@ class AccountRelationshipsPresenter
|
|||
muting: { account_id => muting[account_id] },
|
||||
requested: { account_id => requested[account_id] },
|
||||
requested_by: { account_id => requested_by[account_id] },
|
||||
domain_blocking: { account_id => domain_blocking[account_id] },
|
||||
endorsed: { account_id => endorsed[account_id] },
|
||||
account_note: { account_id => account_note[account_id] },
|
||||
}
|
||||
|
||||
Rails.cache.write("relationship:#{@current_account_id}:#{account_id}", maps_for_account, expires_in: 1.day)
|
||||
[relationship_cache_key(account_id), maps_for_account]
|
||||
end
|
||||
|
||||
Rails.cache.write_multi(to_cache, expires_in: 1.day)
|
||||
end
|
||||
|
||||
def domain_cache_key(domain)
|
||||
['exclude_domains', @current_account_id, domain]
|
||||
end
|
||||
|
||||
def relationship_cache_key(account_id)
|
||||
['relationship', @current_account_id, account_id]
|
||||
end
|
||||
end
|
||||
|
|
|
@ -100,8 +100,8 @@ class InitialStateSerializer < ActiveModel::Serializer
|
|||
|
||||
ActiveRecord::Associations::Preloader.new(
|
||||
records: [object.current_account, object.admin, object.owner, object.disabled_account, object.moved_to_account].compact,
|
||||
associations: [:account_stat, :user, { moved_to_account: [:account_stat, :user] }]
|
||||
)
|
||||
associations: [:account_stat, { user: :role, moved_to_account: [:account_stat, { user: :role }] }]
|
||||
).call
|
||||
|
||||
store[object.current_account.id.to_s] = ActiveModelSerializers::SerializableResource.new(object.current_account, serializer: REST::AccountSerializer) if object.current_account
|
||||
store[object.admin.id.to_s] = ActiveModelSerializers::SerializableResource.new(object.admin, serializer: REST::AccountSerializer) if object.admin
|
||||
|
|
|
@ -242,7 +242,7 @@ class AccountSearchService < BaseService
|
|||
|
||||
records = query_builder.build.limit(limit_for_non_exact_results).offset(offset).objects.compact
|
||||
|
||||
ActiveRecord::Associations::Preloader.new(records: records, associations: :account_stat)
|
||||
ActiveRecord::Associations::Preloader.new(records: records, associations: [:account_stat, { user: :role }]).call
|
||||
|
||||
records
|
||||
rescue Faraday::ConnectionFailed, Parslet::ParseFailed
|
||||
|
|
|
@ -23,9 +23,9 @@ class ActivityPub::FetchFeaturedCollectionService < BaseService
|
|||
|
||||
case collection['type']
|
||||
when 'Collection', 'CollectionPage'
|
||||
collection['items']
|
||||
as_array(collection['items'])
|
||||
when 'OrderedCollection', 'OrderedCollectionPage'
|
||||
collection['orderedItems']
|
||||
as_array(collection['orderedItems'])
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -26,9 +26,9 @@ class ActivityPub::FetchRepliesService < BaseService
|
|||
|
||||
case collection['type']
|
||||
when 'Collection', 'CollectionPage'
|
||||
collection['items']
|
||||
as_array(collection['items'])
|
||||
when 'OrderedCollection', 'OrderedCollectionPage'
|
||||
collection['orderedItems']
|
||||
as_array(collection['orderedItems'])
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -37,7 +37,20 @@ class ActivityPub::FetchRepliesService < BaseService
|
|||
return unless @allow_synchronous_requests
|
||||
return if non_matching_uri_hosts?(@account.uri, collection_or_uri)
|
||||
|
||||
fetch_resource_without_id_validation(collection_or_uri, nil, true)
|
||||
# NOTE: For backward compatibility reasons, Mastodon signs outgoing
|
||||
# queries incorrectly by default.
|
||||
#
|
||||
# While this is relevant for all URLs with query strings, this is
|
||||
# the only code path where this happens in practice.
|
||||
#
|
||||
# Therefore, retry with correct signatures if this fails.
|
||||
begin
|
||||
fetch_resource_without_id_validation(collection_or_uri, nil, true)
|
||||
rescue Mastodon::UnexpectedResponseError => e
|
||||
raise unless e.response && e.response.code == 401 && Addressable::URI.parse(collection_or_uri).query.present?
|
||||
|
||||
fetch_resource_without_id_validation(collection_or_uri, nil, true, request_options: { with_query_string: true })
|
||||
end
|
||||
end
|
||||
|
||||
def filtered_replies
|
||||
|
|
|
@ -59,9 +59,9 @@ class ActivityPub::SynchronizeFollowersService < BaseService
|
|||
|
||||
case collection['type']
|
||||
when 'Collection', 'CollectionPage'
|
||||
collection['items']
|
||||
as_array(collection['items'])
|
||||
when 'OrderedCollection', 'OrderedCollectionPage'
|
||||
collection['orderedItems']
|
||||
as_array(collection['orderedItems'])
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ class BatchedRemoveStatusService < BaseService
|
|||
ActiveRecord::Associations::Preloader.new(
|
||||
records: statuses,
|
||||
associations: options[:skip_side_effects] ? :reblogs : [:account, :tags, reblogs: :account]
|
||||
)
|
||||
).call
|
||||
|
||||
statuses_and_reblogs = statuses.flat_map { |status| [status] + status.reblogs }
|
||||
|
||||
|
@ -23,7 +23,7 @@ class BatchedRemoveStatusService < BaseService
|
|||
ActiveRecord::Associations::Preloader.new(
|
||||
records: statuses_with_account_conversations,
|
||||
associations: [mentions: :account]
|
||||
)
|
||||
).call
|
||||
|
||||
statuses_with_account_conversations.each(&:unlink_from_conversations!)
|
||||
|
||||
|
|
|
@ -100,7 +100,7 @@ class FetchOEmbedService
|
|||
end
|
||||
|
||||
def validate(oembed)
|
||||
oembed if oembed[:version].to_s == '1.0' && oembed[:type].present?
|
||||
oembed if oembed.present? && oembed[:version].to_s == '1.0' && oembed[:type].present?
|
||||
end
|
||||
|
||||
def html
|
||||
|
|
|
@ -69,7 +69,7 @@ class Keys::QueryService < BaseService
|
|||
|
||||
return if json['items'].blank?
|
||||
|
||||
@devices = json['items'].map do |device|
|
||||
@devices = as_array(json['items']).map do |device|
|
||||
Device.new(device_id: device['id'], name: device['name'], identity_key: device.dig('identityKey', 'publicKeyBase64'), fingerprint_key: device.dig('fingerprintKey', 'publicKeyBase64'), claim_url: device['claim'])
|
||||
end
|
||||
rescue HTTP::Error, OpenSSL::SSL::SSLError, Mastodon::Error => e
|
||||
|
|
|
@ -44,11 +44,7 @@ class ReblogService < BaseService
|
|||
def create_notification(reblog)
|
||||
reblogged_status = reblog.reblog
|
||||
|
||||
if reblogged_status.account.local?
|
||||
LocalNotificationWorker.perform_async(reblogged_status.account_id, reblog.id, reblog.class.name, 'reblog')
|
||||
elsif reblogged_status.account.activitypub? && !reblogged_status.account.following?(reblog.account)
|
||||
ActivityPub::DeliveryWorker.perform_async(build_json(reblog), reblog.account_id, reblogged_status.account.inbox_url)
|
||||
end
|
||||
LocalNotificationWorker.perform_async(reblogged_status.account_id, reblog.id, reblog.class.name, 'reblog') if reblogged_status.account.local?
|
||||
end
|
||||
|
||||
def bump_potential_friendship(account, reblog)
|
||||
|
|
|
@ -7,7 +7,7 @@ class LinkCrawlWorker
|
|||
|
||||
def perform(status_id)
|
||||
FetchLinkCardService.new.call(Status.find(status_id))
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
rescue ActiveRecord::RecordNotFound, ActiveRecord::RecordNotUnique
|
||||
true
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue