Skip to content

Commit

Permalink
Wip
Browse files Browse the repository at this point in the history
  • Loading branch information
kmycode committed Feb 21, 2024
1 parent cc7a931 commit f2218be
Show file tree
Hide file tree
Showing 18 changed files with 380 additions and 65 deletions.
2 changes: 1 addition & 1 deletion app/controllers/admin/ng_rule_histories_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ def show
private

def set_ng_rule
@ng_rule = NgRule.find(params[:id])
@ng_rule = ::NgRule.find(params[:id])
end

def set_histories
Expand Down
38 changes: 36 additions & 2 deletions app/controllers/admin/ng_rules_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ def edit
def create
authorize :ng_words, :create?

begin
test_words!
rescue
flash[:alert] = I18n.t('admin.ng_rules.test_error')
redirect_to new_admin_ng_rule_path
return
end

@ng_rule = ::NgRule.build(resource_params)

if @ng_rule.save
Expand All @@ -35,6 +43,14 @@ def create
def update
authorize :ng_words, :create?

begin
test_words!
rescue
flash[:alert] = I18n.t('admin.ng_rules.test_error')
redirect_to edit_admin_ng_rule_path(id: @ng_rule.id)
return
end

if @ng_rule.update(resource_params)
redirect_to admin_ng_rules_path
else
Expand Down Expand Up @@ -62,9 +78,27 @@ def resource_params
:status_sensitive_state, :status_cw_state, :status_media_state, :status_poll_state,
:status_quote_state, :status_reply_state, :status_media_threshold, :status_poll_threshold,
:status_mention_threshold, :status_mention_threshold_stranger_only, :rule_violation_threshold_per_account,
:reaction_type, :reaction_allow_follower, :emoji_reaction_name, :emoji_reaction_origin_domain,
:reaction_allow_follower, :emoji_reaction_name, :emoji_reaction_origin_domain,
:status_reference_threshold, :account_action, :status_action, :reaction_action,
status_visibility: [], status_searchability: [])
status_visibility: [], status_searchability: [], reaction_type: [])
end

def test_words!
arr = [
resource_params[:account_domain],
resource_params[:account_username],
resource_params[:account_display_name],
resource_params[:account_note],
resource_params[:account_field_name],
resource_params[:account_field_value],
resource_params[:status_spoiler_text],
resource_params[:status_text],
resource_params[:status_tag],
resource_params[:emoji_reaction_name],
resource_params[:emoji_reaction_origin_domain],
].compact_blank.join("\n")

Admin::NgRule.extract_test!(arr) if arr.present?
end
end
end
36 changes: 36 additions & 0 deletions app/helpers/ng_rule_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# frozen_string_literal: true

module NgRuleHelper
def check_invalid_status_for_ng_rule!(account, **options)
(check_for_ng_rule!(account, **options) { |rule| !rule.check_status_or_record! }).none? { |rule| rule.account_action == :suspend || rule.status_action == :reject }
end

def check_invalid_reaction_for_ng_rule!(account, **options)
(check_for_ng_rule!(account, **options) { |rule| !rule.check_reaction_or_record! }).none? { |rule| rule.account_action == :suspend || rule.reaction_action == :reject }
end

private

def check_for_ng_rule!(account, **options, &block)
NgRule.cached_rules
.map { |raw_rule| Admin::NgRule.new(raw_rule, account, **options) }
.filter(&block)
.tap do |rules|
account_actions = rules.map(&:account_action).uniq
if account_actions.include? :suspend
do_account_action_for_rule!(account, :suspend)
elsif account_actions.include? :silence
do_account_action_for_rule!(account, :silence)
end
end
end

def do_account_action_for_rule!(account, action)
case action
when :silence
account.silence!
when :suspend
account.suspend!
end
end
end
3 changes: 3 additions & 0 deletions app/lib/activitypub/activity/announce.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# frozen_string_literal: true

class ActivityPub::Activity::Announce < ActivityPub::Activity
include NgRuleHelper

def perform
return reject_payload! if delete_arrived_first?(@json['id']) || !related_to_local_activity?

Expand All @@ -9,6 +11,7 @@ def perform

return reject_payload! if original_status.nil? || !announceable?(original_status)
return if requested_through_relay?
return unless check_invalid_reaction_for_ng_rule! @account, uri: @json['id'], reaction_type: 'reblog', recipient: original_status.account

@status = Status.find_by(account: @account, reblog: original_status)

Expand Down
74 changes: 49 additions & 25 deletions app/models/admin/ng_rule.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,18 @@ def status_match?
state_match?(:status_cw_state, @options[:spoiler_text].present?, @ng_rule.status_cw_state) &&
state_match?(:status_media_state, has_media, @ng_rule.status_media_state) &&
state_match?(:status_poll_state, has_poll, @ng_rule.status_poll_state) &&
state_match?(:status_quote_state, @options[:quote].present?, @ng_rule.status_quote_state) &&
state_match?(:status_reply_state, @options[:reply].presence, @ng_rule.status_reply_state) &&
state_match?(:status_quote_state, @options[:quote], @ng_rule.status_quote_state) &&
state_match?(:status_reply_state, @options[:reply], @ng_rule.status_reply_state) &&
value_over_threshold?(:status_tag_threshold, (@options[:tag_names] || []).size, @ng_rule.status_tag_threshold) &&
value_over_threshold?(:status_media_threshold, @options[:media_count], @ng_rule.status_media_threshold) &&
value_over_threshold?(:status_poll_threshold, @options[:poll_count], @ng_rule.status_poll_threshold) &&
value_over_threshold?(:status_mention_threshold, @options[:mention_count], @ng_rule.status_mention_threshold) &&
value_over_threshold?(:status_reference_threshold, @options[:reference_count], @ng_rule.status_reference_threshold)
end

def reaction_match?
return false if @ng_rule.reaction_allow_follower && @options[:following]
recipient = @options[:recipient]
return false if @ng_rule.reaction_allow_follower && (recipient.id == @account.id || (!recipient.local? && !@account.local?) || recipient.following?(@account))

if @options[:reaction_type] == 'emoji_reaction'
enum_match?(:reaction_type, @options[:reaction_type], @ng_rule.reaction_type) &&
Expand All @@ -66,51 +68,59 @@ def reaction_match?
def check_account_or_record!
return true unless account_match?

record!('account', @account.uri)
record!('account', @account.uri) if !@account.local? || @ng_rule.record_history_also_local

!violation?
end

def check_status_or_record!
return true unless account_match? && status_match?

record!('status', @options[:uri], text: "#{@options[:spoiler_text]}\n\n#{@options[:text]}") if !@options.key?(:visibility) || %i(public public_unlisted login unlsited).include?(@options[:visibility].to_sym)
record!('status', @options[:uri], text: "#{@options[:spoiler_text]}\n\n#{@options[:text]}") if (!@options.key?(:visibility) || %i(public public_unlisted login unlsited).include?(@options[:visibility].to_sym)) && (!@account.local? || @ng_rule.record_history_also_local)

!violation?
end

def check_reaction_or_record!
return true unless account_match? && reaction_match?

record!('reaction', @options[:uri])
record!('reaction', @options[:uri]) if !@account.local? || @ng_rule.record_history_also_local

!violation?
end

private
def account_action
@ng_rule.account_action.to_sym
end

def include?(text, word)
if word.start_with?('?') && word.size >= 2
text =~ /#{word[1..]}/
else
text.include?(word)
end
def status_action
@ng_rule.status_action.to_sym
end

def reaction_action
@ng_rule.reaction_action.to_sym
end

def self.extract_test!(custom_ng_words)
detect_keyword?('test', custom_ng_words)
end

private

def already_did_count
return @already_did_count if defined?(@already_did_count)

@already_did_count = NgRuleHistory.count(ng_rule: @ng_rule, account: @account)
@already_did_count = NgRuleHistory.where(ng_rule: @ng_rule, account: @account).count
end

def violation?
limit = @ng_rule.rule_violation_threshold_per_account
limit = 1 unless limit.is_a?(Integer)
limit = 0 unless limit.is_a?(Integer)

return true if limit.zero?
return false unless limit.positive?
return true if limit <= 1

already_did_count >= limit - 1
already_did_count >= limit
end

def record!(reason, uri, **options)
Expand Down Expand Up @@ -157,17 +167,31 @@ def value_over_threshold?(_reason, value, expected)
value > expected
end

def string_to_array(text)
text.split("\n")
def detect_keyword?(text, arr)
Admin::NgRule.detect_keyword?(text, arr)
end

def detect_keyword(text, arr)
arr = string_to_array(arr) if arr.is_a?(String)
class << self
def string_to_array(text)
text.split("\n")
end

arr.detect { |word| include?(text, word) ? word : nil }
end
def detect_keyword(text, arr)
arr = string_to_array(arr) if arr.is_a?(String)

def detect_keyword?(text, arr)
detect_keyword(text, arr).present?
arr.detect { |word| include?(text, word) ? word : nil }
end

def detect_keyword?(text, arr)
detect_keyword(text, arr).present?
end

def include?(text, word)
if word.start_with?('?') && word.size >= 2
text =~ /#{word[1..]}/
else
text.include?(word)
end
end
end
end
17 changes: 11 additions & 6 deletions app/models/ng_rule.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
# id :bigint(8) not null, primary key
# title :string default(""), not null
# available :boolean default(TRUE), not null
# record_history_also_local :boolean default(TRUE), not null
# account_domain :string default(""), not null
# account_username :string default(""), not null
# account_display_name :string default(""), not null
Expand All @@ -21,12 +22,13 @@
# status_tag :string default(""), not null
# status_visibility :string default([]), not null, is an Array
# status_searchability :string default([]), not null, is an Array
# status_media_state :integer default(0), not null
# status_media_state :integer default("optional"), not null
# status_sensitive_state :integer default("optional"), not null
# status_cw_state :integer default("optional"), not null
# status_poll_state :integer default(0), not null
# status_poll_state :integer default("optional"), not null
# status_quote_state :integer default("optional"), not null
# status_reply_state :integer default("optional"), not null
# status_tag_threshold :integer default(-1), not null
# status_media_threshold :integer default(-1), not null
# status_poll_threshold :integer default(-1), not null
# status_mention_threshold :integer default(-1), not null
Expand All @@ -36,10 +38,10 @@
# reaction_allow_follower :boolean default(TRUE), not null
# emoji_reaction_name :string default(""), not null
# emoji_reaction_origin_domain :string default(""), not null
# rule_violation_threshold_per_account :integer default(1), not null
# account_action :integer default(0), not null
# status_action :integer default(0), not null
# reaction_action :integer default(0), not null
# rule_violation_threshold_per_account :integer default(0), not null
# account_action :integer default("nothing"), not null
# status_action :integer default("nothing"), not null
# reaction_action :integer default("nothing"), not null
# expires_at :datetime
# created_at :datetime not null
# updated_at :datetime not null
Expand All @@ -58,6 +60,9 @@ class NgRule < ApplicationRecord
enum :status_reply_state, { optional: 0, needed: 1, no_needed: 2 }, prefix: :status_reply
enum :status_media_state, { optional: 0, needed: 1, no_needed: 2 }, prefix: :status_media
enum :status_poll_state, { optional: 0, needed: 1, no_needed: 2 }, prefix: :status_poll
enum :account_action, { nothing: 0, silence: 1, suspend: 2 }, prefix: :account_action
enum :status_action, { nothing: 0, reject: 1 }, prefix: :status_action
enum :reaction_action, { nothing: 0, reject: 1 }, prefix: :reaction_action

scope :enabled, -> { where(available: true) }

Expand Down
43 changes: 39 additions & 4 deletions app/services/post_status_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ class PostStatusService < BaseService
include Redisable
include LanguagesHelper
include DtlHelper
include NgRuleHelper

MIN_SCHEDULE_OFFSET = 5.minutes.freeze

Expand Down Expand Up @@ -74,7 +75,7 @@ def preprocess_attributes!
@visibility = :limited if %w(mutual circle reply).include?(@options[:visibility])
@visibility = :unlisted if (@visibility == :public || @visibility == :public_unlisted || @visibility == :login) && @account.silenced?
@visibility = :public_unlisted if @visibility == :public && !@options[:force_visibility] && !@options[:application]&.superapp && @account.user&.setting_public_post_to_unlisted && Setting.enable_public_unlisted_visibility
@visibility = Setting.enable_public_unlisted_visibility ? :public_unlisted : :unlisted unless Setting.enable_public_visibility
@visibility = Setting.enable_public_unlisted_visibility ? :public_unlisted : :unlisted if !Setting.enable_public_visibility && @visibility == :public
@limited_scope = @options[:visibility]&.to_sym if @visibility == :limited && @options[:visibility] != 'limited'
@searchability = searchability
@searchability = :private if @account.silenced? && %i(public public_unlisted).include?(@searchability&.to_sym)
Expand Down Expand Up @@ -149,6 +150,7 @@ def process_status!
process_mentions_service.call(@status, limited_type: @status.limited_visibility? ? @limited_scope : '', circle: @circle, save_records: false)
safeguard_mentions!(@status)
validate_status_mentions!
validate_status_ng_rules!

@status.limited_scope = :personal if @status.limited_visibility? && !@status.reply_limited? && !process_mentions_service.mentions?

Expand Down Expand Up @@ -218,22 +220,55 @@ def validate_status_mentions!
raise Mastodon::ValidationError, I18n.t('statuses.contains_ng_words') if (mention_to_stranger? || reference_to_stranger?) && Setting.stranger_mention_from_local_ng && Admin::NgWord.stranger_mention_reject?("#{@options[:spoiler_text]}\n#{@options[:text]}")
end

def validate_status_ng_rules!
result = check_invalid_status_for_ng_rule! @account,
spoiler_text: @options[:spoiler_text] || '',
text: @text,
tag_names: Extractor.extract_hashtags(@text) || [],
visibility: @visibility.to_s,
searchability: @searchability.to_s,
sensitive: @sensitive,
media_count: (@media || []).size,
poll_count: @options.dig(:poll, 'options')&.size || 0,
quote: quote_url,
reply: @in_reply_to.present?,
mention_count: mention_count,
reference_count: reference_urls.size,
mention_to_stranger: mention_to_stranger? || reference_to_stranger?

raise Mastodon::ValidationError, I18n.t('statuses.violate_rules') unless result
end

def mention_count
@text.gsub(Account::MENTION_RE)&.count || 0
end

def mention_to_stranger?
@status.mentions.map(&:account).to_a.any? { |mentioned_account| mentioned_account.id != @account.id && !mentioned_account.following?(@account) } ||
(@in_reply_to && @in_reply_to.account.id != @account.id && !@in_reply_to.account.following?(@account))
return @mention_to_stranger if defined?(@mention_to_stranger)

@mention_to_stranger = @status.mentions.map(&:account).to_a.any? { |mentioned_account| mentioned_account.id != @account.id && !mentioned_account.following?(@account) } ||
(@in_reply_to && @in_reply_to.account.id != @account.id && !@in_reply_to.account.following?(@account))
end

def reference_to_stranger?
referred_statuses.any? { |status| !status.account.following?(@account) }
end

def referred_statuses
statuses = ProcessReferencesService.extract_uris(@text).filter_map { |uri| ActivityPub::TagManager.instance.local_uri?(uri) && ActivityPub::TagManager.instance.uri_to_resource(uri, Status, url: true) }
statuses = reference_urls.filter_map { |uri| ActivityPub::TagManager.instance.local_uri?(uri) && ActivityPub::TagManager.instance.uri_to_resource(uri, Status, url: true) }
statuses += Status.where(id: @reference_ids) if @reference_ids.present?

statuses
end

def quote_url
ProcessReferencesService.extract_quote(@text)
end

def reference_urls
@reference_urls ||= ProcessReferencesService.extract_uris(@text) || []
end

def validate_media!
if @options[:media_ids].blank? || !@options[:media_ids].is_a?(Enumerable)
@media = []
Expand Down
Loading

0 comments on commit f2218be

Please sign in to comment.