Merge branch 'main' into glitch-soc/merge-upstream

Conflicts:
- `app/controllers/home_controller.rb`:
  Upstream made it so `/web` is available to non-logged-in users
  and `/` redirects to `/web` instead of `/about`.
  Kept our version since glitch-soc's WebUI doesn't have what's
  needed yet and I think /about is still a much better landing
  page anyway.
- `app/models/form/admin_settings.rb`:
  Upstream added new settings, and glitch-soc had an extra setting.
  Not really a conflict.
  Added upstream's new settings.
- `app/serializers/initial_state_serializer.rb`:
  Upstream added a new `server` initial state object.
  Not really a conflict.
  Merged upstream's changes.
- `app/views/admin/settings/edit.html.haml`:
  Upstream added new settings.
  Not really a conflict.
  Merged upstream's changes.
- `app/workers/scheduler/feed_cleanup_scheduler.rb`:
  Upstream refactored that part and removed the file.
  Ported our relevant changes into `app/lib/vacuum/feeds_vacuum.rb`
- `config/settings.yml`:
  Upstream added new settings.
  Not a real conflict.
  Added upstream's new settings.
This commit is contained in:
Claire
2022-10-02 17:33:37 +02:00
390 changed files with 6881 additions and 4298 deletions

View File

@@ -116,12 +116,12 @@ class ActivityPub::Activity
def dereference_object!
return unless @object.is_a?(String)
dereferencer = ActivityPub::Dereferencer.new(@object, permitted_origin: @account.uri, signature_account: signed_fetch_account)
dereferencer = ActivityPub::Dereferencer.new(@object, permitted_origin: @account.uri, signature_actor: signed_fetch_actor)
@object = dereferencer.object unless dereferencer.object.nil?
end
def signed_fetch_account
def signed_fetch_actor
return Account.find(@options[:delivered_to_account_id]) if @options[:delivered_to_account_id].present?
first_mentioned_local_account || first_local_follower
@@ -163,15 +163,15 @@ class ActivityPub::Activity
end
def followed_by_local_accounts?
@account.passive_relationships.exists? || @options[:relayed_through_account]&.passive_relationships&.exists?
@account.passive_relationships.exists? || (@options[:relayed_through_actor].is_a?(Account) && @options[:relayed_through_actor].passive_relationships&.exists?)
end
def requested_through_relay?
@options[:relayed_through_account] && Relay.find_by(inbox_url: @options[:relayed_through_account].inbox_url)&.enabled?
@options[:relayed_through_actor] && Relay.find_by(inbox_url: @options[:relayed_through_actor].inbox_url)&.enabled?
end
def reject_payload!
Rails.logger.info("Rejected #{@json['type']} activity #{@json['id']} from #{@account.uri}#{@options[:relayed_through_account] && "via #{@options[:relayed_through_account].uri}"}")
Rails.logger.info("Rejected #{@json['type']} activity #{@json['id']} from #{@account.uri}#{@options[:relayed_through_actor] && "via #{@options[:relayed_through_actor].uri}"}")
nil
end
end

View File

@@ -3,10 +3,10 @@
class ActivityPub::Dereferencer
include JsonLdHelper
def initialize(uri, permitted_origin: nil, signature_account: nil)
def initialize(uri, permitted_origin: nil, signature_actor: nil)
@uri = uri
@permitted_origin = permitted_origin
@signature_account = signature_account
@signature_actor = signature_actor
end
def object
@@ -46,7 +46,7 @@ class ActivityPub::Dereferencer
req.add_headers('Accept' => 'application/activity+json, application/ld+json')
req.add_headers(headers) if headers
req.on_behalf_of(@signature_account) if @signature_account
req.on_behalf_of(@signature_actor) if @signature_actor
req.perform do |res|
if res.code == 200

View File

@@ -9,7 +9,7 @@ class ActivityPub::LinkedDataSignature
@json = json.with_indifferent_access
end
def verify_account!
def verify_actor!
return unless @json['signature'].is_a?(Hash)
type = @json['signature']['type']
@@ -18,7 +18,7 @@ class ActivityPub::LinkedDataSignature
return unless type == 'RsaSignature2017'
creator = ActivityPub::TagManager.instance.uri_to_resource(creator_uri, Account)
creator = ActivityPub::TagManager.instance.uri_to_actor(creator_uri)
creator ||= ActivityPub::FetchRemoteKeyService.new.call(creator_uri, id: false)
return if creator.nil?
@@ -35,7 +35,7 @@ class ActivityPub::LinkedDataSignature
def sign!(creator, sign_with: nil)
options = {
'type' => 'RsaSignature2017',
'creator' => [ActivityPub::TagManager.instance.uri_for(creator), '#main-key'].join,
'creator' => ActivityPub::TagManager.instance.key_uri_for(creator),
'created' => Time.now.utc.iso8601,
}

View File

@@ -44,6 +44,10 @@ class ActivityPub::TagManager
end
end
def key_uri_for(target)
[uri_for(target), '#main-key'].join
end
def uri_for_username(username)
account_url(username: username)
end
@@ -155,6 +159,10 @@ class ActivityPub::TagManager
path_params[param]
end
def uri_to_actor(uri)
uri_to_resource(uri, Account)
end
def uri_to_resource(uri, klass)
return if uri.nil?

View File

@@ -403,6 +403,7 @@ class FeedManager
def filter_from_home?(status, receiver_id, crutches)
return false if receiver_id == status.account_id
return true if status.reply? && (status.in_reply_to_id.nil? || status.in_reply_to_account_id.nil?)
return true if crutches[:languages][status.account_id].present? && status.language.present? && !crutches[:languages][status.account_id].include?(status.language)
check_for_blocks = crutches[:active_mentions][status.id] || []
check_for_blocks.concat([status.account_id])
@@ -600,6 +601,7 @@ class FeedManager
end
crutches[:following] = Follow.where(account_id: receiver_id, target_account_id: statuses.map(&:in_reply_to_account_id).compact).pluck(:target_account_id).index_with(true)
crutches[:languages] = Follow.where(account_id: receiver_id, target_account_id: statuses.map(&:account_id)).pluck(:target_account_id, :languages).to_h
crutches[:hiding_reblogs] = Follow.where(account_id: receiver_id, target_account_id: statuses.map { |s| s.account_id if s.reblog? }.compact, show_reblogs: false).pluck(:target_account_id).index_with(true)
crutches[:blocking] = Block.where(account_id: receiver_id, target_account_id: check_for_blocks).pluck(:target_account_id).index_with(true)
crutches[:muting] = Mute.where(account_id: receiver_id, target_account_id: check_for_blocks).pluck(:target_account_id).index_with(true)

View File

@@ -17,10 +17,6 @@ class PermalinkRedirector
find_status_url_by_id(path_segments[2])
elsif path_segments[1] == 'accounts' && path_segments[2] =~ /\d/
find_account_url_by_id(path_segments[2])
elsif path_segments[1] == 'timelines' && path_segments[2] == 'tag' && path_segments[3].present?
find_tag_url_by_name(path_segments[3])
elsif path_segments[1] == 'tags' && path_segments[2].present?
find_tag_url_by_name(path_segments[2])
end
end
end

View File

@@ -7,9 +7,7 @@ class RedisConfiguration
@pool = ConnectionPool.new(size: new_pool_size) { new.connection }
end
def with
pool.with { |redis| yield redis }
end
delegate :with, to: :pool
def pool
@pool ||= establish_pool(pool_size)
@@ -17,7 +15,7 @@ class RedisConfiguration
def pool_size
if Sidekiq.server?
Sidekiq.options[:concurrency]
Sidekiq[:concurrency]
else
ENV['MAX_THREADS'] || 5
end

View File

@@ -40,12 +40,11 @@ class Request
set_digest! if options.key?(:body)
end
def on_behalf_of(account, key_id_format = :uri, sign_with: nil)
raise ArgumentError, 'account must not be nil' if account.nil?
def on_behalf_of(actor, sign_with: nil)
raise ArgumentError, 'actor must not be nil' if actor.nil?
@account = account
@keypair = sign_with.present? ? OpenSSL::PKey::RSA.new(sign_with) : @account.keypair
@key_id_format = key_id_format
@actor = actor
@keypair = sign_with.present? ? OpenSSL::PKey::RSA.new(sign_with) : @actor.keypair
self
end
@@ -79,7 +78,7 @@ class Request
end
def headers
(@account ? @headers.merge('Signature' => signature) : @headers).without(REQUEST_TARGET)
(@actor ? @headers.merge('Signature' => signature) : @headers).without(REQUEST_TARGET)
end
class << self
@@ -128,12 +127,7 @@ class Request
end
def key_id
case @key_id_format
when :acct
@account.to_webfinger_s
when :uri
[ActivityPub::TagManager.instance.uri_for(@account), '#main-key'].join
end
ActivityPub::TagManager.instance.key_uri_for(@actor)
end
def http_client
@@ -208,7 +202,7 @@ class Request
addresses.each do |address|
begin
check_private_address(address)
check_private_address(address, host)
sock = ::Socket.new(address.is_a?(Resolv::IPv6) ? ::Socket::AF_INET6 : ::Socket::AF_INET, ::Socket::SOCK_STREAM, 0)
sockaddr = ::Socket.pack_sockaddr_in(port, address.to_s)
@@ -264,10 +258,10 @@ class Request
alias new open
def check_private_address(address)
def check_private_address(address, host)
addr = IPAddr.new(address.to_s)
return if private_address_exceptions.any? { |range| range.include?(addr) }
raise Mastodon::HostValidationError if PrivateAddressCheck.private_address?(addr)
raise Mastodon::PrivateNetworkAddressError, host if PrivateAddressCheck.private_address?(addr)
end
def private_address_exceptions

View File

@@ -0,0 +1,23 @@
# frozen_string_literal: true
class TranslationService
class Error < StandardError; end
class NotConfiguredError < Error; end
class TooManyRequestsError < Error; end
class QuotaExceededError < Error; end
class UnexpectedResponseError < Error; end
def self.configured
if ENV['DEEPL_API_KEY'].present?
TranslationService::DeepL.new(ENV.fetch('DEEPL_PLAN', 'free'), ENV['DEEPL_API_KEY'])
elsif ENV['LIBRE_TRANSLATE_ENDPOINT'].present?
TranslationService::LibreTranslate.new(ENV['LIBRE_TRANSLATE_ENDPOINT'], ENV['LIBRE_TRANSLATE_API_KEY'])
else
raise NotConfiguredError
end
end
def translate(_text, _source_language, _target_language)
raise NotImplementedError
end
end

View File

@@ -0,0 +1,53 @@
# frozen_string_literal: true
class TranslationService::DeepL < TranslationService
include JsonLdHelper
def initialize(plan, api_key)
super()
@plan = plan
@api_key = api_key
end
def translate(text, source_language, target_language)
request(text, source_language, target_language).perform do |res|
case res.code
when 429
raise TooManyRequestsError
when 456
raise QuotaExceededError
when 200...300
transform_response(res.body_with_limit)
else
raise UnexpectedResponseError
end
end
end
private
def request(text, source_language, target_language)
req = Request.new(:post, endpoint_url, form: { text: text, source_lang: source_language&.upcase, target_lang: target_language, tag_handling: 'html' })
req.add_headers('Authorization': "DeepL-Auth-Key #{@api_key}")
req
end
def endpoint_url
if @plan == 'free'
'https://api-free.deepl.com/v2/translate'
else
'https://api.deepl.com/v2/translate'
end
end
def transform_response(str)
json = Oj.load(str, mode: :strict)
raise UnexpectedResponseError unless json.is_a?(Hash)
Translation.new(text: json.dig('translations', 0, 'text'), detected_source_language: json.dig('translations', 0, 'detected_source_language')&.downcase)
rescue Oj::ParseError
raise UnexpectedResponseError
end
end

View File

@@ -0,0 +1,44 @@
# frozen_string_literal: true
class TranslationService::LibreTranslate < TranslationService
def initialize(base_url, api_key)
super()
@base_url = base_url
@api_key = api_key
end
def translate(text, source_language, target_language)
request(text, source_language, target_language).perform do |res|
case res.code
when 429
raise TooManyRequestsError
when 403
raise QuotaExceededError
when 200...300
transform_response(res.body_with_limit, source_language)
else
raise UnexpectedResponseError
end
end
end
private
def request(text, source_language, target_language)
body = Oj.dump(q: text, source: source_language.presence || 'auto', target: target_language, format: 'html', api_key: @api_key)
req = Request.new(:post, "#{@base_url}/translate", body: body)
req.add_headers('Content-Type': 'application/json')
req
end
def transform_response(str, source_language)
json = Oj.load(str, mode: :strict)
raise UnexpectedResponseError unless json.is_a?(Hash)
Translation.new(text: json['translatedText'], detected_source_language: source_language)
rescue Oj::ParseError
raise UnexpectedResponseError
end
end

View File

@@ -0,0 +1,5 @@
# frozen_string_literal: true
class TranslationService::Translation < ActiveModelSerializers::Model
attributes :text, :detected_source_language
end

3
app/lib/vacuum.rb Normal file
View File

@@ -0,0 +1,3 @@
# frozen_string_literal: true
module Vacuum; end

View File

@@ -0,0 +1,18 @@
# frozen_string_literal: true
class Vacuum::AccessTokensVacuum
def perform
vacuum_revoked_access_tokens!
vacuum_revoked_access_grants!
end
private
def vacuum_revoked_access_tokens!
Doorkeeper::AccessToken.where('revoked_at IS NOT NULL').where('revoked_at < NOW()').delete_all
end
def vacuum_revoked_access_grants!
Doorkeeper::AccessGrant.where('revoked_at IS NOT NULL').where('revoked_at < NOW()').delete_all
end
end

View File

@@ -0,0 +1,25 @@
# frozen_string_literal: true
class Vacuum::BackupsVacuum
def initialize(retention_period)
@retention_period = retention_period
end
def perform
vacuum_expired_backups! if retention_period?
end
private
def vacuum_expired_backups!
backups_past_retention_period.in_batches.destroy_all
end
def backups_past_retention_period
Backup.unscoped.where(Backup.arel_table[:created_at].lt(@retention_period.ago))
end
def retention_period?
@retention_period.present?
end
end

View File

@@ -0,0 +1,41 @@
# frozen_string_literal: true
class Vacuum::FeedsVacuum
def perform
vacuum_inactive_home_feeds!
vacuum_inactive_list_feeds!
vacuum_inactive_direct_feeds!
end
private
def vacuum_inactive_home_feeds!
inactive_users.select(:id, :account_id).find_in_batches do |users|
feed_manager.clean_feeds!(:home, users.map(&:account_id))
end
end
def vacuum_inactive_list_feeds!
inactive_users_lists.select(:id).find_in_batches do |lists|
feed_manager.clean_feeds!(:list, lists.map(&:id))
end
end
def vacuum_inactive_direct_feeds!
inactive_users_lists.select(:id).find_in_batches do |lists|
feed_manager.clean_feeds!(:direct, lists.map(&:id))
end
end
def inactive_users
User.confirmed.inactive
end
def inactive_users_lists
List.where(account_id: inactive_users.select(:account_id))
end
def feed_manager
FeedManager.instance
end
end

View File

@@ -0,0 +1,40 @@
# frozen_string_literal: true
class Vacuum::MediaAttachmentsVacuum
TTL = 1.day.freeze
def initialize(retention_period)
@retention_period = retention_period
end
def perform
vacuum_cached_files! if retention_period?
vacuum_orphaned_records!
end
private
def vacuum_cached_files!
media_attachments_past_retention_period.find_each do |media_attachment|
media_attachment.file.destroy
media_attachment.thumbnail.destroy
media_attachment.save
end
end
def vacuum_orphaned_records!
orphaned_media_attachments.in_batches.destroy_all
end
def media_attachments_past_retention_period
MediaAttachment.unscoped.remote.cached.where(MediaAttachment.arel_table[:created_at].lt(@retention_period.ago)).where(MediaAttachment.arel_table[:updated_at].lt(@retention_period.ago))
end
def orphaned_media_attachments
MediaAttachment.unscoped.unattached.where(MediaAttachment.arel_table[:created_at].lt(TTL.ago))
end
def retention_period?
@retention_period.present?
end
end

View File

@@ -0,0 +1,39 @@
# frozen_string_literal: true
class Vacuum::PreviewCardsVacuum
TTL = 1.day.freeze
def initialize(retention_period)
@retention_period = retention_period
end
def perform
vacuum_cached_images! if retention_period?
vacuum_orphaned_records!
end
private
def vacuum_cached_images!
preview_cards_past_retention_period.find_each do |preview_card|
preview_card.image.destroy
preview_card.save
end
end
def vacuum_orphaned_records!
orphaned_preview_cards.in_batches.destroy_all
end
def preview_cards_past_retention_period
PreviewCard.cached.where(PreviewCard.arel_table[:updated_at].lt(@retention_period.ago))
end
def orphaned_preview_cards
PreviewCard.where('NOT EXISTS (SELECT 1 FROM preview_cards_statuses WHERE preview_cards_statuses.preview_card_id = preview_cards.id)').where(PreviewCard.arel_table[:created_at].lt(TTL.ago))
end
def retention_period?
@retention_period.present?
end
end

View File

@@ -0,0 +1,54 @@
# frozen_string_literal: true
class Vacuum::StatusesVacuum
include Redisable
def initialize(retention_period)
@retention_period = retention_period
end
def perform
vacuum_statuses! if retention_period?
end
private
def vacuum_statuses!
statuses_scope.find_in_batches do |statuses|
# Side-effects not covered by foreign keys, such
# as the search index, must be handled first.
remove_from_account_conversations(statuses)
remove_from_search_index(statuses)
# Foreign keys take care of most associated records
# for us. Media attachments will be orphaned.
Status.where(id: statuses.map(&:id)).delete_all
end
end
def statuses_scope
Status.unscoped.kept.where(account: Account.remote).where(Status.arel_table[:id].lt(retention_period_as_id)).select(:id, :visibility)
end
def retention_period_as_id
Mastodon::Snowflake.id_at(@retention_period.ago, with_random: false)
end
def analyze_statuses!
ActiveRecord::Base.connection.execute('ANALYZE statuses')
end
def remove_from_account_conversations(statuses)
Status.where(id: statuses.select(&:direct_visibility?).map(&:id)).includes(:account, mentions: :account).each(&:unlink_from_conversations)
end
def remove_from_search_index(statuses)
with_redis { |redis| redis.sadd('chewy:queue:StatusesIndex', statuses.map(&:id)) } if Chewy.enabled?
end
def retention_period?
@retention_period.present?
end
end

View File

@@ -0,0 +1,13 @@
# frozen_string_literal: true
class Vacuum::SystemKeysVacuum
def perform
vacuum_expired_system_keys!
end
private
def vacuum_expired_system_keys!
SystemKey.expired.delete_all
end
end

View File

@@ -3,7 +3,7 @@
class Webfinger
class Error < StandardError; end
class GoneError < Error; end
class RedirectError < StandardError; end
class RedirectError < Error; end
class Response
attr_reader :uri