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

Conflicts:
- `.github/workflows/build-image.yml`:
  Fix erroneous deletion in a previous merge.
- `Gemfile`:
  Conflict caused by glitch-soc-only hCaptcha dependency
- `app/controllers/auth/sessions_controller.rb`:
  Minor conflict due to glitch-soc's theming system.
- `app/controllers/filters_controller.rb`:
  Minor conflict due to glitch-soc's theming system.
- `app/serializers/rest/status_serializer.rb`:
  Minor conflict due to glitch-soc having an extra `local_only` property
This commit is contained in:
Claire
2022-06-28 11:11:18 +02:00
108 changed files with 2057 additions and 360 deletions

View File

@@ -247,6 +247,19 @@ module AccountInteractions
account_pins.where(target_account: account).exists?
end
def status_matches_filters(status)
active_filters = CustomFilter.cached_filters_for(id)
filter_matches = active_filters.filter_map do |filter, rules|
next if rules[:keywords].blank?
match = rules[:keywords].match(status.proper.searchable_text)
FilterResultPresenter.new(filter: filter, keyword_matches: [match.to_s]) unless match.nil?
end
filter_matches
end
def followers_for_local_distribution
followers.local
.joins(:user)

View File

@@ -3,18 +3,22 @@
#
# Table name: custom_filters
#
# id :bigint(8) not null, primary key
# account_id :bigint(8)
# expires_at :datetime
# phrase :text default(""), not null
# context :string default([]), not null, is an Array
# whole_word :boolean default(TRUE), not null
# irreversible :boolean default(FALSE), not null
# created_at :datetime not null
# updated_at :datetime not null
# id :bigint not null, primary key
# account_id :bigint
# expires_at :datetime
# phrase :text default(""), not null
# context :string default([]), not null, is an Array
# created_at :datetime not null
# updated_at :datetime not null
# action :integer default(0), not null
#
class CustomFilter < ApplicationRecord
self.ignored_columns = %w(whole_word irreversible)
alias_attribute :title, :phrase
alias_attribute :filter_action, :action
VALID_CONTEXTS = %w(
home
notifications
@@ -26,16 +30,20 @@ class CustomFilter < ApplicationRecord
include Expireable
include Redisable
enum action: [:warn, :hide], _suffix: :action
belongs_to :account
has_many :keywords, class_name: 'CustomFilterKeyword', foreign_key: :custom_filter_id, inverse_of: :custom_filter, dependent: :destroy
accepts_nested_attributes_for :keywords, reject_if: :all_blank, allow_destroy: true
validates :phrase, :context, presence: true
validates :title, :context, presence: true
validate :context_must_be_valid
validate :irreversible_must_be_within_context
scope :active_irreversible, -> { where(irreversible: true).where(Arel.sql('expires_at IS NULL OR expires_at > NOW()')) }
before_validation :clean_up_contexts
after_commit :remove_cache
before_save :prepare_cache_invalidation!
before_destroy :prepare_cache_invalidation!
after_commit :invalidate_cache!
def expires_in
return @expires_in if defined?(@expires_in)
@@ -44,22 +52,55 @@ class CustomFilter < ApplicationRecord
[30.minutes, 1.hour, 6.hours, 12.hours, 1.day, 1.week].find { |expires_in| expires_in.from_now >= expires_at }
end
def irreversible=(value)
self.action = value ? :hide : :warn
end
def irreversible?
hide_action?
end
def self.cached_filters_for(account_id)
active_filters = Rails.cache.fetch("filters:v3:#{account_id}") do
scope = CustomFilterKeyword.includes(:custom_filter).where(custom_filter: { account_id: account_id }).where(Arel.sql('expires_at IS NULL OR expires_at > NOW()'))
scope.to_a.group_by(&:custom_filter).map do |filter, keywords|
keywords.map! do |keyword|
if keyword.whole_word
sb = /\A[[:word:]]/.match?(keyword.keyword) ? '\b' : ''
eb = /[[:word:]]\z/.match?(keyword.keyword) ? '\b' : ''
/(?mix:#{sb}#{Regexp.escape(keyword.keyword)}#{eb})/
else
/#{Regexp.escape(keyword.keyword)}/i
end
end
[filter, { keywords: Regexp.union(keywords) }]
end
end.to_a
active_filters.select { |custom_filter, _| !custom_filter.expired? }
end
def prepare_cache_invalidation!
@should_invalidate_cache = true
end
def invalidate_cache!
return unless @should_invalidate_cache
@should_invalidate_cache = false
Rails.cache.delete("filters:v3:#{account_id}")
redis.publish("timeline:#{account_id}", Oj.dump(event: :filters_changed))
redis.publish("timeline:system:#{account_id}", Oj.dump(event: :filters_changed))
end
private
def clean_up_contexts
self.context = Array(context).map(&:strip).filter_map(&:presence)
end
def remove_cache
Rails.cache.delete("filters:#{account_id}")
redis.publish("timeline:#{account_id}", Oj.dump(event: :filters_changed))
end
def context_must_be_valid
errors.add(:context, I18n.t('filters.errors.invalid_context')) if context.empty? || context.any? { |c| !VALID_CONTEXTS.include?(c) }
end
def irreversible_must_be_within_context
errors.add(:irreversible, I18n.t('filters.errors.invalid_irreversible')) if irreversible? && !context.include?('home') && !context.include?('notifications')
end
end

View File

@@ -0,0 +1,34 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: custom_filter_keywords
#
# id :bigint not null, primary key
# custom_filter_id :bigint not null
# keyword :text default(""), not null
# whole_word :boolean default(TRUE), not null
# created_at :datetime not null
# updated_at :datetime not null
#
class CustomFilterKeyword < ApplicationRecord
belongs_to :custom_filter
validates :keyword, presence: true
alias_attribute :phrase, :keyword
before_save :prepare_cache_invalidation!
before_destroy :prepare_cache_invalidation!
after_commit :invalidate_cache!
private
def prepare_cache_invalidation!
custom_filter.prepare_cache_invalidation!
end
def invalidate_cache!
custom_filter.invalidate_cache!
end
end

View File

@@ -11,6 +11,7 @@
#
class DomainAllow < ApplicationRecord
include Paginable
include DomainNormalizable
include DomainMaterializable

View File

@@ -37,6 +37,7 @@ class Notification < ApplicationRecord
poll
update
admin.sign_up
admin.report
).freeze
TARGET_STATUS_INCLUDES_BY_TYPE = {
@@ -46,6 +47,7 @@ class Notification < ApplicationRecord
favourite: [favourite: :status],
poll: [poll: :status],
update: :status,
'admin.report': [report: :target_account],
}.freeze
belongs_to :account, optional: true
@@ -58,6 +60,7 @@ class Notification < ApplicationRecord
belongs_to :follow_request, foreign_key: 'activity_id', optional: true
belongs_to :favourite, foreign_key: 'activity_id', optional: true
belongs_to :poll, foreign_key: 'activity_id', optional: true
belongs_to :report, foreign_key: 'activity_id', optional: true
validates :type, inclusion: { in: TYPES }
@@ -146,7 +149,7 @@ class Notification < ApplicationRecord
return unless new_record?
case activity_type
when 'Status', 'Follow', 'Favourite', 'FollowRequest', 'Poll'
when 'Status', 'Follow', 'Favourite', 'FollowRequest', 'Poll', 'Report'
self.from_account_id = activity&.account_id
when 'Mention'
self.from_account_id = activity&.status&.account_id