* Wip * Wip * Wip: History * Wip: テストコード作成 * Fix test * Wip * Wip * Wip * Fix test * Wip * Wip * Wip * Wip * なんとか完成、これから動作確認 * spell miss * Change ng rule timings * Fix test * Wip * Fix test * Wip * Fix form * 表示まわりの改善
This commit is contained in:
parent
0779c748a6
commit
7d96d5828e
56 changed files with 2062 additions and 42 deletions
24
app/controllers/admin/ng_rule_histories_controller.rb
Normal file
24
app/controllers/admin/ng_rule_histories_controller.rb
Normal file
|
@ -0,0 +1,24 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Admin
|
||||
class NgRuleHistoriesController < BaseController
|
||||
before_action :set_ng_rule
|
||||
before_action :set_histories
|
||||
|
||||
PER_PAGE = 20
|
||||
|
||||
def show
|
||||
authorize :ng_words, :show?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_ng_rule
|
||||
@ng_rule = ::NgRule.find(params[:id])
|
||||
end
|
||||
|
||||
def set_histories
|
||||
@histories = NgRuleHistory.where(ng_rule_id: params[:id]).order(id: :desc).page(params[:page]).per(PER_PAGE)
|
||||
end
|
||||
end
|
||||
end
|
115
app/controllers/admin/ng_rules_controller.rb
Normal file
115
app/controllers/admin/ng_rules_controller.rb
Normal file
|
@ -0,0 +1,115 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Admin
|
||||
class NgRulesController < BaseController
|
||||
before_action :set_ng_rule, only: [:edit, :update, :destroy, :duplicate]
|
||||
|
||||
def index
|
||||
authorize :ng_words, :show?
|
||||
|
||||
@ng_rules = ::NgRule.order(id: :asc)
|
||||
end
|
||||
|
||||
def new
|
||||
authorize :ng_words, :show?
|
||||
|
||||
@ng_rule = ::NgRule.build
|
||||
end
|
||||
|
||||
def edit
|
||||
authorize :ng_words, :show?
|
||||
end
|
||||
|
||||
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
|
||||
redirect_to admin_ng_rules_path
|
||||
else
|
||||
render :new
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
render :edit
|
||||
end
|
||||
end
|
||||
|
||||
def duplicate
|
||||
authorize :ng_words, :create?
|
||||
|
||||
@ng_rule = @ng_rule.copy!
|
||||
|
||||
flash[:alert] = I18n.t('admin.ng_rules.copy_error') unless @ng_rule.save
|
||||
|
||||
redirect_to admin_ng_rules_path
|
||||
end
|
||||
|
||||
def destroy
|
||||
authorize :ng_words, :create?
|
||||
|
||||
@ng_rule.destroy
|
||||
redirect_to admin_ng_rules_path
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_ng_rule
|
||||
@ng_rule = ::NgRule.find(params[:id])
|
||||
end
|
||||
|
||||
def resource_params
|
||||
params.require(:ng_rule).permit(:title, :expires_in, :available, :account_domain, :account_username, :account_display_name,
|
||||
:account_note, :account_field_name, :account_field_value, :account_avatar_state,
|
||||
:account_header_state, :account_include_local, :status_spoiler_text, :status_text, :status_tag,
|
||||
:status_sensitive_state, :status_cw_state, :status_media_state, :status_poll_state,
|
||||
:status_mention_state, :status_reference_state,
|
||||
:status_quote_state, :status_reply_state, :status_media_threshold, :status_poll_threshold,
|
||||
:status_mention_threshold, :status_allow_follower_mention,
|
||||
:reaction_allow_follower, :emoji_reaction_name, :emoji_reaction_origin_domain,
|
||||
:status_reference_threshold, :account_allow_followed_by_local, :record_history_also_local,
|
||||
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
|
28
app/helpers/ng_rule_helper.rb
Normal file
28
app/helpers/ng_rule_helper.rb
Normal file
|
@ -0,0 +1,28 @@
|
|||
# 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?
|
||||
end
|
||||
|
||||
def check_invalid_reaction_for_ng_rule!(account, **options)
|
||||
(check_for_ng_rule!(account, **options) { |rule| !rule.check_reaction_or_record! }).none?
|
||||
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)
|
||||
end
|
||||
|
||||
def do_account_action_for_rule!(account, action)
|
||||
case action
|
||||
when :silence
|
||||
account.silence!
|
||||
when :suspend
|
||||
account.suspend!
|
||||
end
|
||||
end
|
||||
end
|
|
@ -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?
|
||||
|
||||
|
@ -9,6 +11,7 @@ class ActivityPub::Activity::Announce < ActivityPub::Activity
|
|||
|
||||
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, target_status: original_status
|
||||
|
||||
@status = Status.find_by(account: @account, reblog: original_status)
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||
include FormattingHelper
|
||||
include NgRuleHelper
|
||||
|
||||
def perform
|
||||
@account.schedule_refresh_if_stale!
|
||||
|
@ -144,7 +145,9 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
|||
end
|
||||
|
||||
def valid_status?
|
||||
valid = !Admin::NgWord.reject?("#{@params[:spoiler_text]}\n#{@params[:text]}", uri: @params[:uri], target_type: :status, public: @status_parser.distributable_visibility?)
|
||||
valid = true
|
||||
valid = false if valid && !valid_status_for_ng_rule?
|
||||
valid = !Admin::NgWord.reject?("#{@params[:spoiler_text]}\n#{@params[:text]}", uri: @params[:uri], target_type: :status, public: @status_parser.distributable_visibility?) if valid
|
||||
valid = !Admin::NgWord.hashtag_reject?(@tags.size, uri: @params[:uri], target_type: :status, public: @status_parser.distributable_visibility?, text: "#{@params[:spoiler_text]}\n#{@params[:text]}") if valid
|
||||
valid = !Admin::NgWord.mention_reject?(@raw_mention_uris.size, uri: @params[:uri], target_type: :status, public: @status_parser.distributable_visibility?, text: "#{@params[:spoiler_text]}\n#{@params[:text]}") if valid
|
||||
valid = !Admin::NgWord.stranger_mention_reject_with_count?(@raw_mention_uris.size, uri: @params[:uri], target_type: :status, public: @status_parser.distributable_visibility?, text: "#{@params[:spoiler_text]}\n#{@params[:text]}") if valid && (mention_to_local_stranger? || reference_to_local_stranger?)
|
||||
|
@ -154,6 +157,26 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
|||
valid
|
||||
end
|
||||
|
||||
def valid_status_for_ng_rule?
|
||||
check_invalid_status_for_ng_rule! @account,
|
||||
reaction_type: 'create',
|
||||
uri: @params[:uri],
|
||||
url: @params[:url],
|
||||
spoiler_text: @params[:spoiler_text],
|
||||
text: @params[:text],
|
||||
tag_names: @tags.map(&:name),
|
||||
visibility: @params[:visibility].to_s,
|
||||
searchability: @params[:searchability]&.to_s,
|
||||
sensitive: @params[:sensitive],
|
||||
media_count: @params[:media_attachment_ids]&.size,
|
||||
poll_count: @params[:poll]&.options&.size || 0,
|
||||
quote: quote,
|
||||
reply: in_reply_to_uri.present?,
|
||||
mention_count: mentioned_accounts.count,
|
||||
reference_count: reference_uris.size,
|
||||
mention_to_following: !(mention_to_local_stranger? || reference_to_local_stranger?)
|
||||
end
|
||||
|
||||
def accounts_in_audience
|
||||
return @accounts_in_audience if @accounts_in_audience
|
||||
|
||||
|
@ -353,6 +376,8 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
|||
def poll_vote?
|
||||
return false if replied_to_status.nil? || replied_to_status.preloadable_poll.nil? || !replied_to_status.local? || !replied_to_status.preloadable_poll.options.include?(@object['name'])
|
||||
|
||||
return true unless check_invalid_reaction_for_ng_rule! @account, uri: @json['id'], reaction_type: 'vote', recipient: replied_to_status.account, target_status: replied_to_status
|
||||
|
||||
poll_vote! unless replied_to_status.preloadable_poll.expired?
|
||||
|
||||
true
|
||||
|
@ -552,7 +577,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
|||
return @reference_uris if defined?(@reference_uris)
|
||||
|
||||
@reference_uris = @object['references'].nil? ? [] : (ActivityPub::FetchReferencesService.new.call(@account, @object['references']) || []).uniq
|
||||
@reference_uris += ProcessReferencesService.extract_uris(@object['content'] || '')
|
||||
@reference_uris += ProcessReferencesService.extract_uris(@object['content'] || '', remote: true)
|
||||
end
|
||||
|
||||
def local_referred_accounts
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
class ActivityPub::Activity::Follow < ActivityPub::Activity
|
||||
include Payloadable
|
||||
include FollowHelper
|
||||
include NgRuleHelper
|
||||
|
||||
def perform
|
||||
return request_follow_for_friend if friend_follow?
|
||||
|
@ -10,6 +11,7 @@ class ActivityPub::Activity::Follow < ActivityPub::Activity
|
|||
target_account = account_from_uri(object_uri)
|
||||
|
||||
return if target_account.nil? || !target_account.local? || delete_arrived_first?(@json['id'])
|
||||
return unless check_invalid_reaction_for_ng_rule! @account, uri: @json['id'], reaction_type: 'follow', recipient: target_account
|
||||
|
||||
# Update id of already-existing follow requests
|
||||
existing_follow_request = ::FollowRequest.find_by(account: @account, target_account: target_account) || PendingFollowRequest.find_by(account: @account, target_account: target_account)
|
||||
|
|
|
@ -4,6 +4,7 @@ class ActivityPub::Activity::Like < ActivityPub::Activity
|
|||
include Redisable
|
||||
include Lockable
|
||||
include JsonLdHelper
|
||||
include NgRuleHelper
|
||||
|
||||
def perform
|
||||
@original_status = status_from_uri(object_uri)
|
||||
|
@ -25,6 +26,7 @@ class ActivityPub::Activity::Like < ActivityPub::Activity
|
|||
|
||||
def process_favourite
|
||||
return if @account.favourited?(@original_status)
|
||||
return unless check_invalid_reaction_for_ng_rule! @account, uri: @json['id'], reaction_type: 'favourite', recipient: @original_status.account, target_status: @original_status
|
||||
|
||||
favourite = @original_status.favourites.create!(account: @account, uri: @json['id'])
|
||||
|
||||
|
@ -43,6 +45,8 @@ class ActivityPub::Activity::Like < ActivityPub::Activity
|
|||
return if emoji.nil?
|
||||
end
|
||||
|
||||
return unless check_invalid_reaction_for_ng_rule! @account, uri: @json['id'], reaction_type: 'emoji_reaction', emoji_reaction_name: emoji&.shortcode || shortcode, emoji_reaction_origin_domain: emoji&.domain, recipient: @original_status.account, target_status: @original_status
|
||||
|
||||
reaction = nil
|
||||
|
||||
with_redis_lock("emoji_reaction:#{@original_status.id}") do
|
||||
|
|
17
app/lib/vacuum/ng_histories_vacuum.rb
Normal file
17
app/lib/vacuum/ng_histories_vacuum.rb
Normal file
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Vacuum::NgHistoriesVacuum
|
||||
include Redisable
|
||||
|
||||
HISTORY_LIFE_DURATION = 7.days.freeze
|
||||
|
||||
def perform
|
||||
vacuum_histories!
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def vacuum_histories!
|
||||
NgRuleHistory.where('created_at < ?', HISTORY_LIFE_DURATION.ago).in_batches.destroy_all
|
||||
end
|
||||
end
|
205
app/models/admin/ng_rule.rb
Normal file
205
app/models/admin/ng_rule.rb
Normal file
|
@ -0,0 +1,205 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Admin::NgRule
|
||||
def initialize(ng_rule, account, **options)
|
||||
@ng_rule = ng_rule
|
||||
@account = account
|
||||
@options = options
|
||||
@uri = nil
|
||||
end
|
||||
|
||||
def account_match?
|
||||
return false if @account.local? && !@ng_rule.account_include_local
|
||||
return false if !@account.local? && @ng_rule.account_allow_followed_by_local && followed_by_local_accounts?
|
||||
|
||||
if @account.local?
|
||||
return false unless @ng_rule.account_include_local
|
||||
else
|
||||
return false unless text_match?(:account_domain, @account.domain, @ng_rule.account_domain)
|
||||
end
|
||||
|
||||
text_match?(:account_username, @account.username, @ng_rule.account_username) &&
|
||||
text_match?(:account_display_name, @account.display_name, @ng_rule.account_display_name) &&
|
||||
text_match?(:account_note, @account.note, @ng_rule.account_note) &&
|
||||
text_match?(:account_field_name, @account.fields&.map(&:name)&.join("\n"), @ng_rule.account_field_name) &&
|
||||
text_match?(:account_field_value, @account.fields&.map(&:value)&.join("\n"), @ng_rule.account_field_value) &&
|
||||
media_state_match?(:account_avatar_state, @account.avatar, @ng_rule.account_avatar_state) &&
|
||||
media_state_match?(:account_header_state, @account.header, @ng_rule.account_header_state)
|
||||
end
|
||||
|
||||
def status_match? # rubocop:disable Metrics/CyclomaticComplexity
|
||||
return false if @ng_rule.status_allow_follower_mention && @options[:mention_to_following]
|
||||
|
||||
has_media = @options[:media_count].is_a?(Integer) && @options[:media_count].positive?
|
||||
has_poll = @options[:poll_count].is_a?(Integer) && @options[:poll_count].positive?
|
||||
has_mention = @options[:mention_count].is_a?(Integer) && @options[:mention_count].positive?
|
||||
has_reference = @options[:reference_count].is_a?(Integer) && @options[:reference_count].positive?
|
||||
|
||||
@options = @options.merge({ searchability: 'unset' }) if @options[:searchability].nil?
|
||||
|
||||
text_match?(:status_spoiler_text, @options[:spoiler_text], @ng_rule.status_spoiler_text) &&
|
||||
text_match?(:status_text, @options[:text], @ng_rule.status_text) &&
|
||||
text_match?(:status_tag, @options[:tag_names]&.join("\n"), @ng_rule.status_tag) &&
|
||||
enum_match?(:status_visibility, @options[:visibility], @ng_rule.status_visibility) &&
|
||||
enum_match?(:status_searchability, @options[:searchability], @ng_rule.status_searchability) &&
|
||||
state_match?(:status_sensitive_state, @options[:sensitive], @ng_rule.status_sensitive_state) &&
|
||||
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], @ng_rule.status_quote_state) &&
|
||||
state_match?(:status_reply_state, @options[:reply], @ng_rule.status_reply_state) &&
|
||||
state_match?(:status_mention_state, has_mention, @ng_rule.status_mention_state) &&
|
||||
state_match?(:status_reference_state, has_reference, @ng_rule.status_reference_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?
|
||||
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) &&
|
||||
text_match?(:emoji_reaction_name, @options[:emoji_reaction_name], @ng_rule.emoji_reaction_name) &&
|
||||
text_match?(:emoji_reaction_origin_domain, @options[:emoji_reaction_origin_domain], @ng_rule.emoji_reaction_origin_domain)
|
||||
else
|
||||
enum_match?(:reaction_type, @options[:reaction_type], @ng_rule.reaction_type)
|
||||
end
|
||||
end
|
||||
|
||||
def check_account_or_record!
|
||||
return true unless account_match?
|
||||
|
||||
record!('account', @account.uri, 'account_create') if !@account.local? || @ng_rule.record_history_also_local
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
def check_status_or_record!
|
||||
return true unless account_match? && status_match?
|
||||
|
||||
text = [@options[:spoiler_text], @options[:text]].compact_blank.join("\n\n")
|
||||
data = {
|
||||
media_count: @options[:media_count],
|
||||
poll_count: @options[:poll_count],
|
||||
url: @options[:url],
|
||||
}
|
||||
record!('status', @options[:uri], "status_#{@options[:reaction_type]}", text: text, data: data) if !@account.local? || @ng_rule.record_history_also_local
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
def check_reaction_or_record!
|
||||
return true unless account_match? && reaction_match?
|
||||
|
||||
text = @options[:target_status].present? ? [@options[:target_status].spoiler_text, @options[:target_status].text].compact_blank.join("\n\n") : nil
|
||||
data = {
|
||||
url: @options[:target_status].present? ? @options[:target_status].url : nil,
|
||||
}
|
||||
record!('reaction', @options[:uri], "reaction_#{@options[:reaction_type]}", text: text, data: data) if !@account.local? || @ng_rule.record_history_also_local
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
def loggable_visibility?
|
||||
visibility = @options[:target_status]&.visibility || @options[:visibility]
|
||||
return true unless visibility
|
||||
|
||||
%i(public public_unlisted login unlisted).include?(visibility.to_sym)
|
||||
end
|
||||
|
||||
def self.extract_test!(custom_ng_words)
|
||||
detect_keyword?('test', custom_ng_words)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def followed_by_local_accounts?
|
||||
Follow.exists?(account: Account.local, target_account: @account)
|
||||
end
|
||||
|
||||
def record!(reason, uri, reason_action, **options)
|
||||
opts = options.merge({
|
||||
ng_rule: @ng_rule,
|
||||
account: @account,
|
||||
local: @account.local?,
|
||||
reason: reason,
|
||||
reason_action: reason_action,
|
||||
uri: uri,
|
||||
})
|
||||
|
||||
unless loggable_visibility?
|
||||
opts = opts.merge({
|
||||
text: nil,
|
||||
uri: nil,
|
||||
hidden: true,
|
||||
})
|
||||
end
|
||||
|
||||
NgRuleHistory.create!(**opts)
|
||||
end
|
||||
|
||||
def text_match?(_reason, text, arr)
|
||||
return true if arr.blank? || !text.is_a?(String)
|
||||
|
||||
detect_keyword?(text, arr)
|
||||
end
|
||||
|
||||
def enum_match?(_reason, text, arr)
|
||||
return true if !text.is_a?(String) || text.blank?
|
||||
|
||||
arr.include?(text)
|
||||
end
|
||||
|
||||
def state_match?(_reason, exists, expected)
|
||||
case expected.to_sym
|
||||
when :needed
|
||||
exists
|
||||
when :no_needed
|
||||
!exists
|
||||
else
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
def media_state_match?(reason, media, expected)
|
||||
state_match?(reason, media.present?, expected)
|
||||
end
|
||||
|
||||
def value_over_threshold?(_reason, value, expected)
|
||||
return true if !expected.is_a?(Integer) || expected.negative? || !value.is_a?(Integer)
|
||||
|
||||
value > expected
|
||||
end
|
||||
|
||||
def detect_keyword?(text, arr)
|
||||
Admin::NgRule.detect_keyword?(text, arr)
|
||||
end
|
||||
|
||||
class << self
|
||||
def string_to_array(text)
|
||||
text.delete("\r").split("\n")
|
||||
end
|
||||
|
||||
def detect_keyword(text, arr)
|
||||
arr = string_to_array(arr) if arr.is_a?(String)
|
||||
|
||||
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
|
114
app/models/ng_rule.rb
Normal file
114
app/models/ng_rule.rb
Normal file
|
@ -0,0 +1,114 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: ng_rules
|
||||
#
|
||||
# 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
|
||||
# account_note :string default(""), not null
|
||||
# account_field_name :string default(""), not null
|
||||
# account_field_value :string default(""), not null
|
||||
# account_avatar_state :integer default("optional"), not null
|
||||
# account_header_state :integer default("optional"), not null
|
||||
# account_include_local :boolean default(TRUE), not null
|
||||
# account_allow_followed_by_local :boolean default(FALSE), not null
|
||||
# status_spoiler_text :string default(""), not null
|
||||
# status_text :string default(""), not null
|
||||
# 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("optional"), not null
|
||||
# status_sensitive_state :integer default("optional"), not null
|
||||
# status_cw_state :integer default("optional"), 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_mention_state :integer default(0), not null
|
||||
# status_reference_state :integer default(0), 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
|
||||
# status_allow_follower_mention :boolean default(TRUE), not null
|
||||
# status_reference_threshold :integer default(-1), not null
|
||||
# reaction_type :string default([]), not null, is an Array
|
||||
# reaction_allow_follower :boolean default(TRUE), not null
|
||||
# emoji_reaction_name :string default(""), not null
|
||||
# emoji_reaction_origin_domain :string default(""), not null
|
||||
# expires_at :datetime
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
class NgRule < ApplicationRecord
|
||||
include Expireable
|
||||
include Redisable
|
||||
|
||||
has_many :histories, class_name: 'NgRuleHistory', inverse_of: :ng_rule, dependent: :destroy
|
||||
|
||||
enum :account_avatar_state, { optional: 0, needed: 1, no_needed: 2 }, prefix: :account_avatar
|
||||
enum :account_header_state, { optional: 0, needed: 1, no_needed: 2 }, prefix: :account_header
|
||||
enum :status_sensitive_state, { optional: 0, needed: 1, no_needed: 2 }, prefix: :status_sensitive
|
||||
enum :status_cw_state, { optional: 0, needed: 1, no_needed: 2 }, prefix: :status_cw
|
||||
enum :status_quote_state, { optional: 0, needed: 1, no_needed: 2 }, prefix: :status_quote
|
||||
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 :status_mention_state, { optional: 0, needed: 1, no_needed: 2 }, prefix: :status_mention
|
||||
enum :status_reference_state, { optional: 0, needed: 1, no_needed: 2 }, prefix: :status_reference
|
||||
|
||||
scope :enabled, -> { where(available: true) }
|
||||
|
||||
before_validation :clean_up_arrays
|
||||
before_save :prepare_cache_invalidation!
|
||||
before_destroy :prepare_cache_invalidation!
|
||||
after_commit :invalidate_cache!
|
||||
|
||||
def self.cached_rules
|
||||
active_rules = Rails.cache.fetch('ng_rules') do
|
||||
NgRule.enabled.to_a
|
||||
end
|
||||
|
||||
active_rules.reject { |ng_rule, _| ng_rule.expired? }
|
||||
end
|
||||
|
||||
def expires_in
|
||||
return @expires_in if defined?(@expires_in)
|
||||
return nil if expires_at.nil?
|
||||
|
||||
[30.minutes, 1.hour, 6.hours, 12.hours, 1.day, 1.week, 2.weeks, 1.month, 3.months].find { |expires_in| expires_in.from_now >= expires_at }
|
||||
end
|
||||
|
||||
def copy!
|
||||
dup
|
||||
end
|
||||
|
||||
def hit_count
|
||||
Rails.cache.fetch("ng_rule:hit_count:#{id}", expires_in: 15.minutes) { NgRuleHistory.where(ng_rule_id: id).count }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def clean_up_arrays
|
||||
self.status_visibility = Array(status_visibility).map(&:strip).filter_map(&:presence)
|
||||
self.status_searchability = Array(status_searchability).map(&:strip).filter_map(&:presence)
|
||||
self.reaction_type = Array(reaction_type).map(&:strip).filter_map(&:presence)
|
||||
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('ng_rules')
|
||||
end
|
||||
end
|
35
app/models/ng_rule_history.rb
Normal file
35
app/models/ng_rule_history.rb
Normal file
|
@ -0,0 +1,35 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: ng_rule_histories
|
||||
#
|
||||
# id :bigint(8) not null, primary key
|
||||
# ng_rule_id :bigint(8) not null
|
||||
# account_id :bigint(8)
|
||||
# text :string
|
||||
# uri :string
|
||||
# reason :integer not null
|
||||
# reason_action :integer not null
|
||||
# local :boolean default(TRUE), not null
|
||||
# hidden :boolean default(FALSE), not null
|
||||
# data :jsonb
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
class NgRuleHistory < ApplicationRecord
|
||||
enum :reason, { account: 0, status: 1, reaction: 2 }, prefix: :reason
|
||||
enum :reason_action, {
|
||||
account_create: 0,
|
||||
status_create: 10,
|
||||
status_edit: 11,
|
||||
reaction_favourite: 20,
|
||||
reaction_emoji_reaction: 21,
|
||||
reaction_follow: 22,
|
||||
reaction_reblog: 23,
|
||||
reaction_vote: 24,
|
||||
}, prefix: :reason_action
|
||||
|
||||
belongs_to :ng_rule
|
||||
belongs_to :account
|
||||
end
|
|
@ -504,6 +504,10 @@ class Status < ApplicationRecord
|
|||
%w(unset) + selectable_visibilities
|
||||
end
|
||||
|
||||
def all_visibilities
|
||||
visibilities.keys
|
||||
end
|
||||
|
||||
def selectable_searchabilities
|
||||
ss = searchabilities.keys - %w(unsupported)
|
||||
ss -= %w(public_unlisted) unless Setting.enable_public_unlisted_visibility
|
||||
|
@ -514,6 +518,10 @@ class Status < ApplicationRecord
|
|||
searchabilities.keys - %w(public_unlisted unsupported)
|
||||
end
|
||||
|
||||
def all_searchabilities
|
||||
searchabilities.keys - %w(unlisted login unsupported)
|
||||
end
|
||||
|
||||
def favourites_map(status_ids, account_id)
|
||||
Favourite.select('status_id').where(status_id: status_ids).where(account_id: account_id).each_with_object({}) { |f, h| h[f.status_id] = true }
|
||||
end
|
||||
|
|
|
@ -4,6 +4,7 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
|
|||
include JsonLdHelper
|
||||
include Redisable
|
||||
include Lockable
|
||||
include NgRuleHelper
|
||||
|
||||
class AbortError < ::StandardError; end
|
||||
|
||||
|
@ -168,17 +169,38 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
|
|||
end
|
||||
|
||||
def validate_status_mentions!
|
||||
raise AbortError if (mention_to_stranger? || reference_to_stranger?) && Admin::NgWord.stranger_mention_reject?("#{@status_parser.spoiler_text}\n#{@status_parser.text}", uri: @status.uri, target_type: :status)
|
||||
raise AbortError unless valid_status_for_ng_rule?
|
||||
raise AbortError if (mention_to_local_stranger? || reference_to_local_stranger?) && Admin::NgWord.stranger_mention_reject?("#{@status_parser.spoiler_text}\n#{@status_parser.text}", uri: @status.uri, target_type: :status)
|
||||
raise AbortError if Admin::NgWord.mention_reject?(@raw_mentions.size, uri: @status.uri, target_type: :status, text: "#{@status_parser.spoiler_text}\n#{@status_parser.text}")
|
||||
raise AbortError if (mention_to_stranger? || reference_to_stranger?) && Admin::NgWord.stranger_mention_reject_with_count?(@raw_mentions.size, uri: @status.uri, target_type: :status, text: "#{@status_parser.spoiler_text}\n#{@status_parser.text}")
|
||||
raise AbortError if (mention_to_local_stranger? || reference_to_local_stranger?) && Admin::NgWord.stranger_mention_reject_with_count?(@raw_mentions.size, uri: @status.uri, target_type: :status, text: "#{@status_parser.spoiler_text}\n#{@status_parser.text}")
|
||||
end
|
||||
|
||||
def mention_to_stranger?
|
||||
def valid_status_for_ng_rule?
|
||||
check_invalid_status_for_ng_rule! @account,
|
||||
reaction_type: 'edit',
|
||||
uri: @status.uri,
|
||||
url: @status_parser.url || @status.url,
|
||||
spoiler_text: @status.spoiler_text,
|
||||
text: @status.text,
|
||||
tag_names: @raw_tags,
|
||||
visibility: @status.visibility,
|
||||
searchability: @status.searchability,
|
||||
sensitive: @status.sensitive,
|
||||
media_count: @next_media_attachments.size,
|
||||
poll_count: @status.poll&.options&.size || 0,
|
||||
quote: quote,
|
||||
reply: @status.reply?,
|
||||
mention_count: @status.mentions.count,
|
||||
reference_count: reference_uris.size,
|
||||
mention_to_following: !(mention_to_local_stranger? || reference_to_local_stranger?)
|
||||
end
|
||||
|
||||
def mention_to_local_stranger?
|
||||
@status.mentions.map(&:account).to_a.any? { |mentioned_account| mentioned_account.id != @status.account.id && mentioned_account.local? && !mentioned_account.following?(@status.account) } ||
|
||||
(@status.thread.present? && @status.thread.account.id != @status.account.id && @status.thread.account.local? && !@status.thread.account.following?(@status.account))
|
||||
end
|
||||
|
||||
def reference_to_stranger?
|
||||
def reference_to_local_stranger?
|
||||
local_referred_accounts.any? { |account| !account.following?(@account) }
|
||||
end
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ class EmojiReactService < BaseService
|
|||
include Payloadable
|
||||
include Redisable
|
||||
include Lockable
|
||||
include NgRuleHelper
|
||||
|
||||
# React a status with emoji and notify remote user
|
||||
# @param [Account] account
|
||||
|
@ -18,9 +19,12 @@ class EmojiReactService < BaseService
|
|||
|
||||
raise Mastodon::ValidationError, I18n.t('reactions.errors.banned') if account.silenced? && !status.account.following?(account)
|
||||
|
||||
shortcode, domain = name.split('@')
|
||||
domain = nil if TagManager.instance.local_domain?(domain)
|
||||
|
||||
raise Mastodon::ValidationError, I18n.t('statuses.violate_rules') unless check_invalid_reaction_for_ng_rule! account, reaction_type: 'emoji_reaction', emoji_reaction_name: shortcode, emoji_reaction_origin_domain: domain, recipient: status.account, target_status: status
|
||||
|
||||
with_redis_lock("emoji_reaction:#{status.id}") do
|
||||
shortcode, domain = name.split('@')
|
||||
domain = nil if TagManager.instance.local_domain?(domain)
|
||||
custom_emoji = CustomEmoji.find_by(shortcode: shortcode, domain: domain)
|
||||
return if domain.present? && !EmojiReaction.exists?(status: status, custom_emoji: custom_emoji)
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
class FavouriteService < BaseService
|
||||
include Authorization
|
||||
include Payloadable
|
||||
include NgRuleHelper
|
||||
|
||||
# Favourite a status and notify remote user
|
||||
# @param [Account] account
|
||||
|
@ -11,6 +12,8 @@ class FavouriteService < BaseService
|
|||
def call(account, status)
|
||||
authorize_with account, status, :favourite?
|
||||
|
||||
raise Mastodon::ValidationError, I18n.t('statuses.violate_rules') unless check_invalid_reaction_for_ng_rule! account, reaction_type: 'favourite', recipient: status.account, target_status: status
|
||||
|
||||
favourite = Favourite.find_by(account: account, status: status)
|
||||
|
||||
return favourite unless favourite.nil?
|
||||
|
|
|
@ -4,6 +4,7 @@ class FollowService < BaseService
|
|||
include Redisable
|
||||
include Payloadable
|
||||
include DomainControlHelper
|
||||
include NgRuleHelper
|
||||
|
||||
# Follow a remote user, notify remote user about the follow
|
||||
# @param [Account] source_account From which to follow
|
||||
|
@ -23,6 +24,8 @@ class FollowService < BaseService
|
|||
raise ActiveRecord::RecordNotFound if following_not_possible?
|
||||
raise Mastodon::NotPermittedError if following_not_allowed?
|
||||
|
||||
raise Mastodon::ValidationError, I18n.t('statuses.violate_rules') unless check_invalid_reaction_for_ng_rule! @source_account, reaction_type: 'follow', recipient: @target_account
|
||||
|
||||
if @source_account.following?(@target_account)
|
||||
return change_follow_options!
|
||||
elsif @source_account.requested?(@target_account)
|
||||
|
|
|
@ -4,6 +4,7 @@ class PostStatusService < BaseService
|
|||
include Redisable
|
||||
include LanguagesHelper
|
||||
include DtlHelper
|
||||
include NgRuleHelper
|
||||
|
||||
MIN_SCHEDULE_OFFSET = 5.minutes.freeze
|
||||
|
||||
|
@ -74,7 +75,7 @@ class PostStatusService < BaseService
|
|||
@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)
|
||||
|
@ -148,6 +149,7 @@ class PostStatusService < BaseService
|
|||
@status = @account.statuses.new(status_attributes)
|
||||
process_mentions_service.call(@status, limited_type: @status.limited_visibility? ? @limited_scope : '', circle: @circle, save_records: false)
|
||||
safeguard_mentions!(@status)
|
||||
validate_status_ng_rules!
|
||||
validate_status_mentions!
|
||||
|
||||
@status.limited_scope = :personal if @status.limited_visibility? && !@status.reply_limited? && !process_mentions_service.mentions?
|
||||
|
@ -218,9 +220,35 @@ class PostStatusService < BaseService
|
|||
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,
|
||||
reaction_type: 'create',
|
||||
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_following: !(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?
|
||||
|
@ -228,12 +256,20 @@ class PostStatusService < BaseService
|
|||
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 = []
|
||||
|
|
|
@ -8,6 +8,7 @@ class ProcessReferencesService < BaseService
|
|||
|
||||
DOMAIN = ENV['WEB_DOMAIN'] || ENV.fetch('LOCAL_DOMAIN', nil)
|
||||
REFURL_EXP = /(RT|QT|BT|RN|RE)((:|;)?\s+|:|;)(#{URI::DEFAULT_PARSER.make_regexp(%w(http https))})/
|
||||
QUOTEURL_EXP = /(QT|RN|RE)((:|;)?\s+|:|;)(#{URI::DEFAULT_PARSER.make_regexp(%w(http https))})/
|
||||
MAX_REFERENCES = 5
|
||||
|
||||
def call(status, reference_parameters, urls: nil, fetch_remote: true, no_fetch_urls: nil, quote_urls: nil)
|
||||
|
@ -45,8 +46,14 @@ class ProcessReferencesService < BaseService
|
|||
reference_parameters.any? || (urls || []).any? || (quote_urls || []).any? || FormattingHelper.extract_status_plain_text(status).scan(REFURL_EXP).pluck(3).uniq.any?
|
||||
end
|
||||
|
||||
def self.extract_uris(text)
|
||||
text.scan(REFURL_EXP).pluck(3)
|
||||
def self.extract_uris(text, remote: false)
|
||||
return text.scan(REFURL_EXP).pluck(3) unless remote
|
||||
|
||||
PlainTextFormatter.new(text, false).to_s.scan(REFURL_EXP).pluck(3)
|
||||
end
|
||||
|
||||
def self.extract_quote(text)
|
||||
text.scan(QUOTEURL_EXP).pick(3)
|
||||
end
|
||||
|
||||
def self.perform_worker_async(status, reference_parameters, urls, quote_urls)
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
class ReblogService < BaseService
|
||||
include Authorization
|
||||
include Payloadable
|
||||
include NgRuleHelper
|
||||
|
||||
# Reblog a status and notify its remote author
|
||||
# @param [Account] account Account to reblog from
|
||||
|
@ -16,6 +17,8 @@ class ReblogService < BaseService
|
|||
|
||||
authorize_with account, reblogged_status, :reblog?
|
||||
|
||||
raise Mastodon::ValidationError, I18n.t('statuses.violate_rules') unless check_invalid_reaction_for_ng_rule! account, reaction_type: 'reblog', recipient: reblogged_status.account, target_status: reblogged_status
|
||||
|
||||
reblog = account.statuses.find_by(reblog: reblogged_status)
|
||||
|
||||
return reblog unless reblog.nil?
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
class UpdateStatusService < BaseService
|
||||
include Redisable
|
||||
include LanguagesHelper
|
||||
include NgRuleHelper
|
||||
|
||||
class NoChangesSubmittedError < StandardError; end
|
||||
|
||||
|
@ -31,6 +32,8 @@ class UpdateStatusService < BaseService
|
|||
validate_status!
|
||||
|
||||
Status.transaction do
|
||||
validate_status_ng_rules!
|
||||
|
||||
create_previous_edit! unless @options[:no_history]
|
||||
update_media_attachments! if @options.key?(:media_ids)
|
||||
update_poll! if @options.key?(:poll)
|
||||
|
@ -91,6 +94,30 @@ class UpdateStatusService < BaseService
|
|||
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! @status.account,
|
||||
reaction_type: 'edit',
|
||||
spoiler_text: @options.key?(:spoiler_text) ? (@options[:spoiler_text] || '') : @status.spoiler_text,
|
||||
text: text,
|
||||
tag_names: Extractor.extract_hashtags(text) || [],
|
||||
visibility: @status.visibility,
|
||||
searchability: @status.searchability,
|
||||
sensitive: @options.key?(:sensitive) ? @options[:sensitive] : @status.sensitive,
|
||||
media_count: @options[:media_ids].present? ? @options[:media_ids].size : @status.media_attachments.count,
|
||||
poll_count: @options.dig(:poll, 'options')&.size || 0,
|
||||
quote: quote_url,
|
||||
reply: @status.reply?,
|
||||
mention_count: mention_count,
|
||||
reference_count: reference_urls.size,
|
||||
mention_to_following: !(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 != @status.account.id && !mentioned_account.following?(@status.account) } ||
|
||||
(@status.thread.present? && @status.thread.account.id != @status.account.id && !@status.thread.account.following?(@status.account))
|
||||
|
@ -101,9 +128,21 @@ class UpdateStatusService < BaseService
|
|||
end
|
||||
|
||||
def referred_statuses
|
||||
return [] unless @options[:text]
|
||||
return [] unless text
|
||||
|
||||
ProcessReferencesService.extract_uris(@options[:text]).filter_map { |uri| ActivityPub::TagManager.instance.local_uri?(uri) && ActivityPub::TagManager.instance.uri_to_resource(uri, Status, url: true) }
|
||||
reference_urls.filter_map { |uri| ActivityPub::TagManager.instance.local_uri?(uri) && ActivityPub::TagManager.instance.uri_to_resource(uri, Status, url: true) }
|
||||
end
|
||||
|
||||
def quote_url
|
||||
ProcessReferencesService.extract_quote(text)
|
||||
end
|
||||
|
||||
def reference_urls
|
||||
@reference_urls ||= ProcessReferencesService.extract_uris(text) || []
|
||||
end
|
||||
|
||||
def text
|
||||
@options.key?(:text) ? (@options[:text] || '') : @status.text
|
||||
end
|
||||
|
||||
def validate_media!
|
||||
|
|
|
@ -5,6 +5,7 @@ class VoteService < BaseService
|
|||
include Payloadable
|
||||
include Redisable
|
||||
include Lockable
|
||||
include NgRuleHelper
|
||||
|
||||
def call(account, poll, choices)
|
||||
return if choices.empty?
|
||||
|
@ -16,6 +17,8 @@ class VoteService < BaseService
|
|||
@choices = choices
|
||||
@votes = []
|
||||
|
||||
raise Mastodon::ValidationError, I18n.t('statuses.violate_rules') unless check_invalid_reaction_for_ng_rule! @account, reaction_type: 'vote', recipient: @poll.status.account, target_status: @poll.status
|
||||
|
||||
already_voted = true
|
||||
|
||||
with_redis_lock("vote:#{@poll.id}:#{@account.id}") do
|
||||
|
|
42
app/views/admin/ng_rule_histories/_history.html.haml
Normal file
42
app/views/admin/ng_rule_histories/_history.html.haml
Normal file
|
@ -0,0 +1,42 @@
|
|||
.batch-table__row
|
||||
%label.batch-table__row__select.batch-checkbox
|
||||
-# = f.check_box :history_ids, { multiple: true, include_hidden: false }, history.id
|
||||
.batch-table__row__content
|
||||
- if history.hidden
|
||||
.simple_form
|
||||
%p.hint= t('admin.ng_rule_histories.hidden')
|
||||
- else
|
||||
.status__content><
|
||||
= html_aware_format(history.text, history.local)
|
||||
|
||||
.detailed-status__meta
|
||||
= t("admin.ng_rule_histories.reason_actions.#{history.reason_action}")
|
||||
- if history.data.present? && !history.hidden
|
||||
- if history.data['media_count'].present? && history.data['media_count'].positive?
|
||||
·
|
||||
= t('admin.ng_rule_histories.data.media_count', count: history.data['media_count'])
|
||||
- if history.data['poll_count'].present? && history.data['poll_count'].positive?
|
||||
·
|
||||
= t('admin.ng_rule_histories.data.poll_count', count: history.data['poll_count'])
|
||||
|
||||
%br/
|
||||
|
||||
- if history.account.present?
|
||||
- if history.hidden
|
||||
- if history.account.local?
|
||||
= t('admin.ng_rule_histories.from_local_user')
|
||||
- else
|
||||
= history.account.domain
|
||||
·
|
||||
- else
|
||||
= link_to t('admin.ng_rule_histories.moderate_account'), admin_account_path(history.account.id)
|
||||
·
|
||||
|
||||
%time.formatted{ datetime: history.created_at.iso8601, title: l(history.created_at) }= l(history.created_at)
|
||||
|
||||
- if history.uri.present? && !history.hidden
|
||||
·
|
||||
- if history.data.present? && history.data['url'].present?
|
||||
= link_to history.uri, history.data['url'] || history.uri, target: '_blank', rel: 'noopener'
|
||||
- else
|
||||
= link_to history.uri, target: '_blank', rel: 'noopener'
|
25
app/views/admin/ng_rule_histories/show.html.haml
Normal file
25
app/views/admin/ng_rule_histories/show.html.haml
Normal file
|
@ -0,0 +1,25 @@
|
|||
- content_for :page_title do
|
||||
= t('admin.ng_rule_histories.title', title: @ng_rule.title)
|
||||
|
||||
.filters
|
||||
.back-link
|
||||
= link_to edit_admin_ng_rule_path(id: @ng_rule.id) do
|
||||
= fa_icon 'chevron-left fw'
|
||||
= t('admin.ng_rule_histories.back_to_ng_rule')
|
||||
= link_to admin_ng_rules_path do
|
||||
= fa_icon 'chevron-left fw'
|
||||
= t('admin.ng_rule_histories.back_to_ng_rules')
|
||||
|
||||
%hr.spacer/
|
||||
|
||||
.batch-table
|
||||
.batch-table__toolbar
|
||||
%label.batch-table__toolbar__select.batch-checkbox-all
|
||||
= check_box_tag :batch_checkbox_all, nil, false
|
||||
.batch-table__body
|
||||
- if @histories.empty?
|
||||
= nothing_here 'nothing-here--under-tabs'
|
||||
- else
|
||||
= render partial: 'admin/ng_rule_histories/history', collection: @histories
|
||||
|
||||
= paginate @histories
|
25
app/views/admin/ng_rules/_ng_rule.html.haml
Normal file
25
app/views/admin/ng_rules/_ng_rule.html.haml
Normal file
|
@ -0,0 +1,25 @@
|
|||
.filters-list__item{ class: [(ng_rule.expired? || !ng_rule.available) && 'expired'] }
|
||||
= link_to edit_admin_ng_rule_path(ng_rule), class: 'filters-list__item__title' do
|
||||
= ng_rule.title.presence || "(#{t('admin.ng_rules.index.empty_title')})"
|
||||
|
||||
- if ng_rule.expires?
|
||||
.expiration{ title: t('filters.index.expires_on', date: l(ng_rule.expires_at)) }
|
||||
- if ng_rule.expired?
|
||||
= t('invites.expired')
|
||||
- else
|
||||
= t('filters.index.expires_in', distance: distance_of_time_in_words_to_now(ng_rule.expires_at))
|
||||
- elsif !ng_rule.available
|
||||
.expiration
|
||||
= t('admin.ng_rules.index.disabled')
|
||||
|
||||
.filters-list__item__permissions
|
||||
%ul.permissions-list
|
||||
|
||||
.announcements-list__item__action-bar
|
||||
.announcements-list__item__meta
|
||||
= link_to t('admin.ng_rules.index.hit_count', count: ng_rule.hit_count), admin_ng_rule_history_path(ng_rule)
|
||||
|
||||
%div
|
||||
= table_link_to 'pencil', t('admin.ng_rules.index.edit.title'), edit_admin_ng_rule_path(ng_rule)
|
||||
= table_link_to 'files-o', t('admin.ng_rules.copy'), duplicate_admin_ng_rule_path(ng_rule), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }
|
||||
= table_link_to 'times', t('admin.ng_rules.index.delete'), admin_ng_rule_path(ng_rule), method: :delete, data: { confirm: t('admin.accounts.are_you_sure') }
|
123
app/views/admin/ng_rules/_ng_rule_fields.html.haml
Normal file
123
app/views/admin/ng_rules/_ng_rule_fields.html.haml
Normal file
|
@ -0,0 +1,123 @@
|
|||
|
||||
.fields-group
|
||||
%p= t('admin.ng_rules.edit.helps.generic')
|
||||
|
||||
.fields-group
|
||||
%p
|
||||
= t('admin.ng_rules.edit.helps.textarea_html')
|
||||
= link_to t('admin.ng_rules.rubular'), 'https://rubular.com/', target: '_blank', rel: 'noopener'
|
||||
|
||||
.fields-group
|
||||
%p= t('admin.ng_rules.edit.helps.threshold_html')
|
||||
|
||||
- if @ng_rule.id.present?
|
||||
%p.hint= link_to t('admin.ng_rules.edit.history'), admin_ng_rule_history_path(id: @ng_rule.id)
|
||||
|
||||
%hr.spacer/
|
||||
|
||||
.fields-row
|
||||
.fields-row__column.fields-row__column-6.fields-group
|
||||
= f.input :title, as: :string, wrapper: :with_label, hint: false
|
||||
.fields-row__column.fields-row__column-6.fields-group
|
||||
= f.input :expires_in, wrapper: :with_label, collection: [30.minutes, 1.hour, 6.hours, 12.hours, 1.day, 1.week, 2.weeks, 1.month, 3.months].map(&:to_i), label_method: ->(i) { I18n.t("invites.expires_in.#{i}") }, include_blank: I18n.t('invites.expires_in_prompt')
|
||||
|
||||
%h4= t('admin.ng_rules.edit.headers.account')
|
||||
%p.lead= t('admin.ng_rules.edit.summary.account')
|
||||
|
||||
.fields-group
|
||||
= f.input :account_allow_followed_by_local, as: :boolean, wrapper: :with_label, hint: t('admin.ng_rules.account_allow_followed_by_local_hint'), label: t('admin.ng_rules.account_allow_followed_by_local')
|
||||
|
||||
.fields-row
|
||||
.fields-row__column.fields-row__column-6.fields-group
|
||||
= f.input :account_domain, as: :text, input_html: { rows: 4 }, wrapper: :with_label, label: t('admin.ng_rules.account_domain'), hint: false
|
||||
.fields-row__column.fields-row__column-6.fields-group
|
||||
= f.input :account_include_local, as: :boolean, wrapper: :with_label, label: t('admin.ng_rules.account_include_local'), hint: false
|
||||
|
||||
.fields-row
|
||||
.fields-row__column.fields-row__column-6.fields-group
|
||||
= f.input :account_username, as: :text, input_html: { rows: 4 }, wrapper: :with_label, label: t('admin.ng_rules.account_username'), hint: false
|
||||
.fields-row__column.fields-row__column-6.fields-group
|
||||
= f.input :account_display_name, as: :text, input_html: { rows: 4 }, wrapper: :with_label, label: t('admin.ng_rules.account_display_name'), hint: false
|
||||
|
||||
.fields-row
|
||||
.fields-row__column.fields-row__column-6.fields-group
|
||||
= f.input :account_field_name, as: :text, input_html: { rows: 4 }, wrapper: :with_label, label: t('admin.ng_rules.account_field_name'), hint: false
|
||||
.fields-row__column.fields-row__column-6.fields-group
|
||||
= f.input :account_field_value, as: :text, input_html: { rows: 4 }, wrapper: :with_label, label: t('admin.ng_rules.account_field_value'), hint: false
|
||||
|
||||
.fields-row
|
||||
.fields-row__column.fields-row__column-6.fields-group
|
||||
= f.input :account_note, as: :text, input_html: { rows: 4 }, wrapper: :with_label, label: t('admin.ng_rules.account_note'), hint: false
|
||||
.fields-row__column.fields-row__column-6.fields-group
|
||||
= f.input :account_avatar_state, wrapper: :with_label, collection: %i(optional needed no_needed), include_blank: false, label_method: ->(i) { I18n.t("admin.ng_rules.states.#{i}") }, label: t('admin.ng_rules.account_avatar_state')
|
||||
= f.input :account_header_state, wrapper: :with_label, collection: %i(optional needed no_needed), include_blank: false, label_method: ->(i) { I18n.t("admin.ng_rules.states.#{i}") }, label: t('admin.ng_rules.account_header_state')
|
||||
|
||||
%h4= t('admin.ng_rules.edit.headers.status')
|
||||
%p.lead= t('admin.ng_rules.edit.summary.status')
|
||||
|
||||
.fields-group
|
||||
= f.input :status_allow_follower_mention, as: :boolean, wrapper: :with_label, hint: t('admin.ng_rules.status_allow_follower_mention_hint'), label: t('admin.ng_rules.status_allow_follower_mention')
|
||||
|
||||
.fields-row
|
||||
.fields-row__column.fields-row__column-6.fields-group
|
||||
= f.input :status_text, as: :text, input_html: { rows: 4 }, wrapper: :with_label, label: t('admin.ng_rules.status_text'), hint: false
|
||||
.fields-row__column.fields-row__column-6.fields-group
|
||||
= f.input :status_spoiler_text, as: :text, input_html: { rows: 4 }, wrapper: :with_label, label: t('admin.ng_rules.status_spoiler_text'), hint: false
|
||||
|
||||
.fields-row
|
||||
.fields-row__column.fields-row__column-6.fields-group
|
||||
= f.input :status_tag, as: :text, input_html: { rows: 4 }, wrapper: :with_label, label: t('admin.ng_rules.status_tag'), hint: false
|
||||
.fields-row__column.fields-row__column-6.fields-group
|
||||
= f.input :status_sensitive_state, wrapper: :with_label, collection: %i(optional needed no_needed), include_blank: false, label_method: ->(i) { I18n.t("admin.ng_rules.states.#{i}") }, label: t('admin.ng_rules.status_sensitive_state')
|
||||
= f.input :status_cw_state, wrapper: :with_label, collection: %i(optional needed no_needed), include_blank: false, label_method: ->(i) { I18n.t("admin.ng_rules.states.#{i}") }, label: t('admin.ng_rules.status_cw_state')
|
||||
|
||||
.fields-group
|
||||
= f.input :status_visibility, wrapper: :with_block_label, collection: Status.all_visibilities, as: :check_boxes, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li', label_method: ->(context) { I18n.t("statuses.visibilities.#{context}") }, include_blank: false, label: t('admin.ng_rules.status_visibility')
|
||||
|
||||
.fields-group
|
||||
= f.input :status_searchability, wrapper: :with_block_label, collection: Status.all_searchabilities + %w(unset), as: :check_boxes, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li', label_method: ->(context) { I18n.t("statuses.searchabilities.#{context}") }, include_blank: false, label: t('admin.ng_rules.status_searchability')
|
||||
|
||||
.fields-row
|
||||
.fields-row__column.fields-row__column-6.fields-group
|
||||
= f.input :status_media_state, wrapper: :with_label, collection: %i(optional needed no_needed), include_blank: false, label_method: ->(i) { I18n.t("admin.ng_rules.states.#{i}") }, label: t('admin.ng_rules.status_media_state')
|
||||
= f.input :status_media_threshold, as: :string, wrapper: :with_label, hint: false, label: t('admin.ng_rules.status_media_threshold')
|
||||
.fields-row__column.fields-row__column-6.fields-group
|
||||
= f.input :status_poll_state, wrapper: :with_label, collection: %i(optional needed no_needed), include_blank: false, label_method: ->(i) { I18n.t("admin.ng_rules.states.#{i}") }, label: t('admin.ng_rules.status_poll_state')
|
||||
= f.input :status_poll_threshold, as: :string, wrapper: :with_label, hint: false, label: t('admin.ng_rules.status_poll_threshold')
|
||||
|
||||
.fields-row
|
||||
.fields-row__column.fields-row__column-6.fields-group
|
||||
= f.input :status_quote_state, wrapper: :with_label, collection: %i(optional needed no_needed), include_blank: false, label_method: ->(i) { I18n.t("admin.ng_rules.states.#{i}") }, label: t('admin.ng_rules.status_quote_state')
|
||||
.fields-row__column.fields-row__column-6.fields-group
|
||||
= f.input :status_reply_state, wrapper: :with_label, collection: %i(optional needed no_needed), include_blank: false, label_method: ->(i) { I18n.t("admin.ng_rules.states.#{i}") }, label: t('admin.ng_rules.status_reply_state')
|
||||
|
||||
.fields-row
|
||||
.fields-row__column.fields-row__column-6.fields-group
|
||||
= f.input :status_mention_state, wrapper: :with_label, collection: %i(optional needed no_needed), include_blank: false, label_method: ->(i) { I18n.t("admin.ng_rules.states.#{i}") }, label: t('admin.ng_rules.status_mention_state')
|
||||
= f.input :status_mention_threshold, as: :string, wrapper: :with_label, hint: false, label: t('admin.ng_rules.status_mention_threshold')
|
||||
.fields-row__column.fields-row__column-6.fields-group
|
||||
= f.input :status_reference_state, wrapper: :with_label, collection: %i(optional needed no_needed), include_blank: false, label_method: ->(i) { I18n.t("admin.ng_rules.states.#{i}") }, label: t('admin.ng_rules.status_reference_state')
|
||||
= f.input :status_reference_threshold, as: :string, wrapper: :with_label, hint: false, label: t('admin.ng_rules.status_reference_threshold')
|
||||
|
||||
%h4= t('admin.ng_rules.edit.headers.reaction')
|
||||
%p.lead= t('admin.ng_rules.edit.summary.reaction')
|
||||
|
||||
.fields-row
|
||||
= f.input :reaction_allow_follower, wrapper: :with_label, hint: t('admin.ng_rules.reaction_allow_follower_hint'), label: t('admin.ng_rules.reaction_allow_follower')
|
||||
|
||||
.fields-group
|
||||
= f.input :reaction_type, wrapper: :with_block_label, collection: %i(favourite emoji_reaction reblog follow vote), as: :check_boxes, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li', label_method: ->(context) { I18n.t("admin.ng_rules.reaction_types.#{context}") }, include_blank: false, label: t('admin.ng_rules.reaction_type')
|
||||
|
||||
.fields-row
|
||||
.fields-row__column.fields-row__column-6.fields-group
|
||||
= f.input :emoji_reaction_name, as: :text, input_html: { rows: 4 }, wrapper: :with_label, hint: false, label: t('admin.ng_rules.emoji_reaction_name')
|
||||
.fields-row__column.fields-row__column-6.fields-group
|
||||
= f.input :emoji_reaction_origin_domain, as: :text, input_html: { rows: 4 }, wrapper: :with_label, hint: t('admin.ng_rules.emoji_reaction_origin_domain_hint'), label: t('admin.ng_rules.emoji_reaction_origin_domain')
|
||||
|
||||
%h4= t('admin.ng_rules.edit.headers.options')
|
||||
|
||||
.fields-group
|
||||
= f.input :available, wrapper: :with_label, label: t('admin.ng_rules.available'), hint: false
|
||||
|
||||
.fields-group
|
||||
= f.input :record_history_also_local, wrapper: :with_label, label: t('admin.ng_rules.record_history_also_local'), hint: false
|
9
app/views/admin/ng_rules/edit.html.haml
Normal file
9
app/views/admin/ng_rules/edit.html.haml
Normal file
|
@ -0,0 +1,9 @@
|
|||
- content_for :page_title do
|
||||
= t('admin.ng_rules.edit.title')
|
||||
|
||||
= simple_form_for @ng_rule, url: admin_ng_rule_path(@ng_rule), method: :put do |f|
|
||||
= render 'shared/error_messages', object: @ng_rule
|
||||
= render 'ng_rule_fields', f: f
|
||||
|
||||
.actions
|
||||
= f.button :button, t('generic.save_changes'), type: :submit
|
14
app/views/admin/ng_rules/index.html.haml
Normal file
14
app/views/admin/ng_rules/index.html.haml
Normal file
|
@ -0,0 +1,14 @@
|
|||
- content_for :page_title do
|
||||
= t('admin.ng_rules.index.title')
|
||||
|
||||
- content_for :heading_actions do
|
||||
= link_to t('admin.ng_rules.new.title'), new_admin_ng_rule_path, class: 'button'
|
||||
|
||||
.simple_form
|
||||
%p.lead= t('admin.ng_rules.index.preamble')
|
||||
|
||||
- if @ng_rules.empty?
|
||||
.muted-hint.center-text= t 'admin.ng_rules.index.empty'
|
||||
- else
|
||||
.applications-list
|
||||
= render partial: 'ng_rule', collection: @ng_rules
|
8
app/views/admin/ng_rules/new.html.haml
Normal file
8
app/views/admin/ng_rules/new.html.haml
Normal file
|
@ -0,0 +1,8 @@
|
|||
- content_for :page_title do
|
||||
= t('admin.ng_rules.new.title')
|
||||
|
||||
= simple_form_for @ng_rule, url: admin_ng_rules_path do |f|
|
||||
= render 'ng_rule_fields', f: f
|
||||
|
||||
.actions
|
||||
= f.button :button, t('admin.ng_rules.new.save'), type: :submit
|
|
@ -7,6 +7,8 @@
|
|||
= simple_form_for @admin_settings, url: admin_ng_words_path, html: { method: :post } do |f|
|
||||
= render 'shared/error_messages', object: @admin_settings
|
||||
|
||||
%p.lead= t('admin.ng_words.preamble')
|
||||
|
||||
%p.hint
|
||||
= t 'admin.ng_words.history_hint'
|
||||
= link_to t('admin.ngword_histories.title'), admin_ngword_histories_path
|
||||
|
@ -14,9 +16,6 @@
|
|||
.fields-group
|
||||
= f.input :ng_words_for_stranger_mention, wrapper: :with_label, as: :text, input_html: { rows: 10 }, label: t('admin.ng_words.keywords_for_stranger_mention'), hint: t('admin.ng_words.keywords_for_stranger_mention_hint')
|
||||
|
||||
.fields-group
|
||||
= f.input :stranger_mention_from_local_ng, wrapper: :with_label, as: :boolean, label: t('admin.ng_words.stranger_mention_from_local_ng'), hint: t('admin.ng_words.stranger_mention_from_local_ng_hint')
|
||||
|
||||
.fields-group
|
||||
= f.input :ng_words, wrapper: :with_label, as: :text, input_html: { rows: 10 }, label: t('admin.ng_words.keywords'), hint: t('admin.ng_words.keywords_hint')
|
||||
|
||||
|
@ -29,21 +28,28 @@
|
|||
.fields-group
|
||||
= f.input :post_mentions_max, wrapper: :with_label, as: :integer, label: t('admin.ng_words.post_mentions_max')
|
||||
|
||||
.fields-group
|
||||
= f.input :hide_local_users_for_anonymous, wrapper: :with_label, as: :boolean, label: t('admin.ng_words.hide_local_users_for_anonymous')
|
||||
|
||||
.fields-group
|
||||
= f.input :block_unfollow_account_mention, wrapper: :with_label, as: :boolean, label: t('admin.ng_words.block_unfollow_account_mention')
|
||||
|
||||
%p.hint
|
||||
= t 'admin.ng_words.remote_approval_hint'
|
||||
%h4= t('admin.ng_words.white_list')
|
||||
%p.lead
|
||||
= t('admin.ng_words.white_list_hint')
|
||||
= link_to t('admin.ng_words.remote_approval_list'), admin_accounts_path(status: 'remote_pending', origin: 'remote')
|
||||
|
||||
.fields-group
|
||||
= f.input :hold_remote_new_accounts, wrapper: :with_label, as: :boolean, label: t('admin.ng_words.hold_remote_new_accounts')
|
||||
= f.input :hold_remote_new_accounts, wrapper: :with_label, as: :boolean, label: t('admin.ng_words.hold_remote_new_accounts'), hint: t('admin.ng_words.remote_approval_hint')
|
||||
|
||||
.fields-group
|
||||
= f.input :permit_new_account_domains, wrapper: :with_label, as: :text, kmyblue: true, input_html: { rows: 6 }, label: t('admin.ng_words.permit_new_account_domains')
|
||||
|
||||
%h4= t('admin.ng_words.deprecated')
|
||||
%p.hint= t('admin.ng_words.deprecated_hint')
|
||||
|
||||
.fields-group
|
||||
= f.input :stranger_mention_from_local_ng, wrapper: :with_label, as: :boolean, label: t('admin.ng_words.stranger_mention_from_local_ng'), hint: t('admin.ng_words.stranger_mention_from_local_ng_hint')
|
||||
|
||||
.fields-group
|
||||
= f.input :hide_local_users_for_anonymous, wrapper: :with_label, as: :boolean, label: t('admin.ng_words.hide_local_users_for_anonymous'), hint: t('admin.ng_words.hide_local_users_for_anonymous_hint')
|
||||
|
||||
.fields-group
|
||||
= f.input :block_unfollow_account_mention, wrapper: :with_label, as: :boolean, label: t('admin.ng_words.block_unfollow_account_mention'), hint: t('admin.ng_words.block_unfollow_account_mention_hint')
|
||||
|
||||
.actions
|
||||
= f.button :button, t('generic.save_changes'), type: :submit
|
||||
|
|
|
@ -4,16 +4,16 @@
|
|||
- content_for :header_tags do
|
||||
= javascript_pack_tag 'admin', async: true, crossorigin: 'anonymous'
|
||||
|
||||
%p.hint= t 'admin.sensitive_words.hint'
|
||||
|
||||
= simple_form_for @admin_settings, url: admin_sensitive_words_path, html: { method: :post } do |f|
|
||||
= render 'shared/error_messages', object: @admin_settings
|
||||
|
||||
.fields-group
|
||||
= f.input :sensitive_words_for_full, wrapper: :with_label, as: :text, input_html: { rows: 12 }, label: t('admin.sensitive_words.keywords_for_all'), hint: t('admin.sensitive_words.keywords_for_all_hint')
|
||||
%p.lead= t 'admin.sensitive_words.hint'
|
||||
|
||||
.fields-group
|
||||
= f.input :sensitive_words, wrapper: :with_label, as: :text, input_html: { rows: 12 }, label: t('admin.sensitive_words.keywords'), hint: t('admin.sensitive_words.keywords_hint')
|
||||
= f.input :sensitive_words_for_full, wrapper: :with_label, as: :text, input_html: { rows: 8 }, label: t('admin.sensitive_words.keywords_for_all'), hint: t('admin.sensitive_words.keywords_for_all_hint')
|
||||
|
||||
.fields-group
|
||||
= f.input :sensitive_words, wrapper: :with_label, as: :text, input_html: { rows: 8 }, label: t('admin.sensitive_words.keywords'), hint: t('admin.sensitive_words.keywords_hint')
|
||||
|
||||
.actions
|
||||
= f.button :button, t('generic.save_changes'), type: :submit
|
||||
|
|
|
@ -26,6 +26,7 @@ class Scheduler::VacuumScheduler
|
|||
feeds_vacuum,
|
||||
imports_vacuum,
|
||||
list_statuses_vacuum,
|
||||
ng_histories_vacuum,
|
||||
]
|
||||
end
|
||||
|
||||
|
@ -65,6 +66,10 @@ class Scheduler::VacuumScheduler
|
|||
Vacuum::ApplicationsVacuum.new
|
||||
end
|
||||
|
||||
def ng_histories_vacuum
|
||||
Vacuum::NgHistoriesVacuum.new
|
||||
end
|
||||
|
||||
def content_retention_policy
|
||||
ContentRetentionPolicy.current
|
||||
end
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue