decidim-spam_signal 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (93) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.md +661 -0
  3. data/README.md +277 -0
  4. data/Rakefile +93 -0
  5. data/app/commands/decidim/comments/create_comment.rb +75 -0
  6. data/app/commands/decidim/spam_signal/admin/add_rule_command.rb +48 -0
  7. data/app/commands/decidim/spam_signal/admin/add_scanner_command.rb +46 -0
  8. data/app/commands/decidim/spam_signal/admin/remove_cop_command.rb +31 -0
  9. data/app/commands/decidim/spam_signal/admin/remove_rule_command.rb +31 -0
  10. data/app/commands/decidim/spam_signal/admin/remove_scanner_command.rb +31 -0
  11. data/app/commands/decidim/spam_signal/admin/update_cop_command.rb +41 -0
  12. data/app/commands/decidim/spam_signal/admin/update_rule_command.rb +47 -0
  13. data/app/commands/decidim/spam_signal/admin/update_scanner_command.rb +47 -0
  14. data/app/commands/decidim/spam_signal/application_handler.rb +27 -0
  15. data/app/commands/decidim/spam_signal/command.rb +14 -0
  16. data/app/commands/decidim/spam_signal/cops/cop_handler.rb +72 -0
  17. data/app/commands/decidim/spam_signal/cops/lock_cop_command.rb +58 -0
  18. data/app/commands/decidim/spam_signal/cops/sinalize_cop_command.rb +25 -0
  19. data/app/commands/decidim/spam_signal/scans/allowed_tlds_scan_command.rb +50 -0
  20. data/app/commands/decidim/spam_signal/scans/forbidden_tlds_scan_command.rb +49 -0
  21. data/app/commands/decidim/spam_signal/scans/scan_handler.rb +24 -0
  22. data/app/commands/decidim/spam_signal/scans/word_scan_command.rb +40 -0
  23. data/app/controllers/concerns/decidim/user_blocked_checker.rb +48 -0
  24. data/app/controllers/decidim/spam_signal/admin/application_controller.rb +18 -0
  25. data/app/controllers/decidim/spam_signal/admin/application_cops_controller.rb +122 -0
  26. data/app/controllers/decidim/spam_signal/admin/application_rules_controller.rb +136 -0
  27. data/app/controllers/decidim/spam_signal/admin/application_scans_controller.rb +110 -0
  28. data/app/controllers/decidim/spam_signal/admin/comment_cops_controller.rb +13 -0
  29. data/app/controllers/decidim/spam_signal/admin/comment_rules_controller.rb +13 -0
  30. data/app/controllers/decidim/spam_signal/admin/comment_scans_controller.rb +13 -0
  31. data/app/controllers/decidim/spam_signal/admin/profile_cops_controller.rb +13 -0
  32. data/app/controllers/decidim/spam_signal/admin/profile_rules_controller.rb +13 -0
  33. data/app/controllers/decidim/spam_signal/admin/profile_scans_controller.rb +13 -0
  34. data/app/controllers/decidim/spam_signal/admin/spam_filter_reports_controller.rb +41 -0
  35. data/app/forms/decidim/spam_signal/cops/lock_settings_form.rb +14 -0
  36. data/app/forms/decidim/spam_signal/cops/no_settings_form.rb +11 -0
  37. data/app/forms/decidim/spam_signal/cops/sinalize_settings_form.rb +13 -0
  38. data/app/forms/decidim/spam_signal/no_settings_form.rb +9 -0
  39. data/app/forms/decidim/spam_signal/rule_form.rb +10 -0
  40. data/app/forms/decidim/spam_signal/scans/allowed_tlds_form.rb +13 -0
  41. data/app/forms/decidim/spam_signal/scans/forbidden_tlds_form.rb +13 -0
  42. data/app/forms/decidim/spam_signal/scans/word_settings_form.rb +13 -0
  43. data/app/forms/decidim/spam_signal/settings_form.rb +27 -0
  44. data/app/helpers/decidim/spam_signal/admin/spam_signal_helper.rb +22 -0
  45. data/app/helpers/decidim/spam_signal/application_helper.rb +10 -0
  46. data/app/models/decidim/spam_signal/config.rb +48 -0
  47. data/app/overrides/profiles_noindex.rb +17 -0
  48. data/app/packs/images/decidim/spam_signal/entrypoints/spam_signal.js +1 -0
  49. data/app/packs/images/decidim/spam_signal/icon.svg +1 -0
  50. data/app/repositories/decidim/spam_signal/spam_config_repo.rb +160 -0
  51. data/app/views/decidim/admin/moderated_users/_report.html.erb +15 -0
  52. data/app/views/decidim/admin/moderated_users/index.html.erb +82 -0
  53. data/app/views/decidim/comments/comments/error.js.erb +4 -0
  54. data/app/views/decidim/spam_signal/admin/comment_cops/edit.html.erb +9 -0
  55. data/app/views/decidim/spam_signal/admin/comment_rules/edit.html.erb +8 -0
  56. data/app/views/decidim/spam_signal/admin/comment_rules/new.html.erb +9 -0
  57. data/app/views/decidim/spam_signal/admin/comment_scans/edit.html.erb +8 -0
  58. data/app/views/decidim/spam_signal/admin/comment_scans/new.html.erb +8 -0
  59. data/app/views/decidim/spam_signal/admin/profile_cops/edit.html.erb +9 -0
  60. data/app/views/decidim/spam_signal/admin/profile_rules/edit.html.erb +8 -0
  61. data/app/views/decidim/spam_signal/admin/profile_rules/new.html.erb +9 -0
  62. data/app/views/decidim/spam_signal/admin/profile_scans/edit.html.erb +7 -0
  63. data/app/views/decidim/spam_signal/admin/profile_scans/new.html.erb +8 -0
  64. data/app/views/decidim/spam_signal/admin/shared/cops/_edit.html.erb +48 -0
  65. data/app/views/decidim/spam_signal/admin/shared/rules/_edit.html.erb +27 -0
  66. data/app/views/decidim/spam_signal/admin/shared/rules/_new.html.erb +28 -0
  67. data/app/views/decidim/spam_signal/admin/shared/scans/_edit.html.erb +25 -0
  68. data/app/views/decidim/spam_signal/admin/shared/scans/_new.html.erb +39 -0
  69. data/app/views/decidim/spam_signal/admin/spam_filter_reports/index.html.erb +282 -0
  70. data/config/assets.rb +8 -0
  71. data/config/i18n-tasks.yml +9 -0
  72. data/config/initializers/spam_signal.rb +7 -0
  73. data/config/locales/en.yml +114 -0
  74. data/config/locales/fr.yml +115 -0
  75. data/config/routes.rb +3 -0
  76. data/lib/decidim/spam_signal/admin.rb +10 -0
  77. data/lib/decidim/spam_signal/admin_engine.rb +42 -0
  78. data/lib/decidim/spam_signal/cop_bot.rb +41 -0
  79. data/lib/decidim/spam_signal/cops/cops_repository.rb +37 -0
  80. data/lib/decidim/spam_signal/engine.rb +23 -0
  81. data/lib/decidim/spam_signal/extractors/comment_extractor.rb +15 -0
  82. data/lib/decidim/spam_signal/extractors/extractor.rb +13 -0
  83. data/lib/decidim/spam_signal/extractors/profile_extractor.rb +15 -0
  84. data/lib/decidim/spam_signal/scans/scans_repository.rb +44 -0
  85. data/lib/decidim/spam_signal/spam_settings_form_builder.rb +22 -0
  86. data/lib/decidim/spam_signal/test/factories.rb +24 -0
  87. data/lib/decidim/spam_signal/test/scan_factories.rb +17 -0
  88. data/lib/decidim/spam_signal/validators/comment_spam_validator.rb +119 -0
  89. data/lib/decidim/spam_signal/validators/profile_spam_validator.rb +133 -0
  90. data/lib/decidim/spam_signal/version.rb +14 -0
  91. data/lib/decidim/spam_signal.rb +31 -0
  92. data/lib/tasks/antispam.rb +23 -0
  93. metadata +210 -0
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails"
4
+ require "decidim/core"
5
+ require "deface"
6
+
7
+ module Decidim
8
+ module SpamSignal
9
+ # This is the engine that runs on the public interface of spam_signal.
10
+ class Engine < ::Rails::Engine
11
+ isolate_namespace Decidim::SpamSignal
12
+
13
+ config.to_prepare do
14
+ Decidim::User.include(ProfileSpamValidator)
15
+ Decidim::Comments::CommentForm.include(CommentSpamValidator)
16
+ end
17
+
18
+ initializer "decidim_spam_signal.webpacker.assets_path" do
19
+ Decidim.register_assets_path File.expand_path("#{Decidim::SpamSignal::Engine.root}/app/packs")
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module SpamSignal
5
+ module Extractors
6
+ class CommentExtractor < Extractor
7
+ def self.extract(comment, config)
8
+ body = comment.attributes[:body]
9
+ return "" unless body.present?
10
+ body
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module SpamSignal
5
+ module Extractors
6
+ class Extractor
7
+ def self.extract(model, config)
8
+ raise Error, "not implemented"
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module SpamSignal
5
+ module Extractors
6
+ class ProfileExtractor < Extractor
7
+ def self.extract(user, _config)
8
+ "#{user.about}" + (user.personal_url ? "
9
+ ===
10
+ #{user.personal_url}" : "")
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "singleton"
4
+
5
+ module Decidim
6
+ module SpamSignal
7
+ module Scans
8
+ ##
9
+ # Strategies repository
10
+ class ScansRepository
11
+ include Singleton
12
+ attr_reader :strategies
13
+
14
+ def initialize
15
+ @_strategies = {}
16
+ end
17
+
18
+ def register(strategy, command_klass)
19
+ @_strategies[strategy.to_sym] = command_klass
20
+ end
21
+
22
+ def unset_strategy(strategy)
23
+ key = strategy.to_sym
24
+ raise Error, "Cop's Strategy #{strategy} does not exists" unless @_strategies.key? key
25
+ @_strategies.except!(key)
26
+ end
27
+
28
+ def strategies
29
+ @_strategies.keys
30
+ end
31
+
32
+ def form_for(strategy)
33
+ strategy(strategy).form
34
+ end
35
+
36
+ def strategy(strategy)
37
+ key = "#{strategy}".to_sym
38
+ return @_strategies[key] if @_strategies.key? key
39
+ nil
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module SpamSignal
5
+ class SpamSettingsFormBuilder < Decidim::AuthorizationFormBuilder
6
+ def input_field(name, type, **options)
7
+ return hidden_field(name) if name.to_s == "handler_name"
8
+ case type
9
+ when :date, :datetime, :time, :"decidim/attributes/localized_date"
10
+ date_field name
11
+ when :integer, Integer
12
+ number_field name
13
+ else
14
+ return text_area name, rows: 5 if name.to_s.ends_with? "_csv"
15
+ return number_field name if name.to_s.starts_with? "num_"
16
+ return check_box name if name.to_s.starts_with?("is_") || name.to_s.ends_with?("enabled")
17
+ text_field name
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "decidim/faker/localized"
4
+ require "decidim/faker/internet"
5
+ require "decidim/dev"
6
+ require "decidim/core/test/factories"
7
+ require_relative "scan_factories"
8
+ FactoryBot.define do
9
+ factory :spam_signal_config, class: "Decidim::SpamSignal::Config" do
10
+ organization { create(:organization) }
11
+ comment_settings { {
12
+ "scans" => {},
13
+ "rules" => {},
14
+ "spam_cop" => {},
15
+ "suspicious_cop" => {}
16
+ } }
17
+ profile_settings { {
18
+ "scans" => {},
19
+ "rules" => {},
20
+ "spam_cop" => {},
21
+ "suspicious_cop" => {}
22
+ }}
23
+ end
24
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ class SpamScan < Decidim::SpamSignal::Scans::ScanHandler
4
+ def call
5
+ broadcast(:spam)
6
+ end
7
+ end
8
+ class SuspiciousScan < Decidim::SpamSignal::Scans::ScanHandler
9
+ def call
10
+ broadcast(:suspicious)
11
+ end
12
+ end
13
+ class OkScan < Decidim::SpamSignal::Scans::ScanHandler
14
+ def call
15
+ broadcast(:ok)
16
+ end
17
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module SpamSignal
5
+ module CommentSpamValidator
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ validate :scan_spam
10
+
11
+ def scan_spam
12
+ return if body.empty?
13
+ tested_content = Extractors::CommentExtractor.extract(self, spam_config)
14
+ # Collect fired symbols
15
+ symboles = spam_scanners.map do |scan_command, scan_option|
16
+ scan_command.call(
17
+ tested_content,
18
+ scan_option
19
+ )
20
+ end.map(&:keys).flatten.filter { |s| s != :ok && s != :valid }
21
+ return if !symboles || symboles.empty?
22
+
23
+ # Check the rules
24
+ suspicious_rules = spam_ruleset("suspicious")
25
+ spam_rules = spam_ruleset("spam")
26
+ fire_suspicious_cop = suspicious_rules.map do |ruleset|
27
+ ruleset.map do |rule|
28
+ symboles.include? rule
29
+ end.all?
30
+ end.any?
31
+ fire_spam_cop = spam_rules.map do |ruleset|
32
+ ruleset.map do |rule|
33
+ symboles.include? rule
34
+ end.all?
35
+ end.any?
36
+ # If it's a cop fire it, else if it's a suspicous, fire it.
37
+ if fire_spam_cop && obvious_spam_cop
38
+ obvious_spam_cop.call(
39
+ errors: errors,
40
+ suspicious_user: author,
41
+ config: obvious_spam_cop_options,
42
+ reportable: author,
43
+ justification: "comment: " + tested_content
44
+ )
45
+ elsif fire_suspicious_cop && suspicious_spam_cop
46
+ suspicious_spam_cop.call(
47
+ errors: errors,
48
+ suspicious_user: author,
49
+ reportable: author,
50
+ config: suspicious_spam_cop_options,
51
+ justification: "comment: " + tested_content
52
+ )
53
+ end
54
+ end
55
+
56
+ def obvious_spam_cop_options
57
+ cop = spam_config.comments.spam_cop
58
+ return nil unless cop
59
+ cop
60
+ end
61
+
62
+ def obvious_spam_cop
63
+ cop_key = obvious_spam_cop_options["handler_name"]
64
+ return nil unless cop_key
65
+ Cops::CopsRepository.instance.strategy(
66
+ cop_key
67
+ )
68
+ end
69
+
70
+ def suspicious_spam_cop_options
71
+ cop = spam_config.comments.suspicious_cop
72
+ return nil unless cop
73
+ cop
74
+ end
75
+
76
+ def suspicious_spam_cop
77
+ cop_key = suspicious_spam_cop_options["handler_name"]
78
+ return nil unless cop_key
79
+ Cops::CopsRepository.instance.strategy(
80
+ cop_key
81
+ )
82
+ end
83
+
84
+ def spam_config
85
+ @config ||= Config.get_config(context.current_organization)
86
+ end
87
+
88
+ def author
89
+ context.author || nil
90
+ end
91
+
92
+ def scan_context
93
+ {
94
+ validator: commentable.commentable_type == "Decidim::Comments::Comment" ? "comment-reply" : "comment",
95
+ is_updating: id.present?,
96
+ date: id.present? ? updated_at : DateTime.now,
97
+ current_organization: context.current_organization,
98
+ author: context.author
99
+ }
100
+ end
101
+
102
+ def spam_scanners
103
+ spam_config.comments.scans.map do |s, options|
104
+ options["context"] = scan_context
105
+ [Scans::ScansRepository.instance.strategy(s), options]
106
+ end
107
+ end
108
+
109
+ def spam_ruleset(handler_name = "spam")
110
+ spam_config.comments.rules.map do |rule_id, rule|
111
+ rule
112
+ end.filter { |r| r["handler_name"] == handler_name }.map do |r|
113
+ r["rules"].symbolize_keys.keys
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module SpamSignal
5
+ module ProfileSpamValidator
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ validate :scan_spam
10
+ def scan_spam
11
+ return if !about || about.empty?
12
+ return if blocked_at_changed?(from: nil) # we are blocking the user, don't validate if it is spam.
13
+ current_user = self
14
+ tested_content = Extractors::ProfileExtractor.extract(self, spam_config)
15
+
16
+ # Collect fired symbols
17
+ symboles = spam_scanners.map do |scan_command, scan_option|
18
+ scan_command.call(
19
+ tested_content,
20
+ scan_option
21
+ )
22
+ end.map(&:keys).flatten.filter { |s| s != :ok && s != :valid }
23
+ return if !symboles || symboles.empty?
24
+
25
+ # Check the rules
26
+ suspicious_rules = spam_ruleset("suspicious")
27
+ spam_rules = spam_ruleset("spam")
28
+ fire_suspicious_cop = suspicious_rules.map do |ruleset|
29
+ ruleset.map do |rule|
30
+ symboles.include? rule
31
+ end.all?
32
+ end.any?
33
+ fire_spam_cop = spam_rules.map do |ruleset|
34
+ ruleset.map do |rule|
35
+ symboles.include? rule
36
+ end.all?
37
+ end.any?
38
+
39
+ # If it's a cop fire it, else if it's a suspicous, fire it.
40
+ if fire_spam_cop && obvious_spam_cop
41
+ obvious_spam_cop.call(
42
+ errors: errors,
43
+ error_key: :about,
44
+ suspicious_user: current_user,
45
+ config: obvious_spam_cop_options,
46
+ justification: "profile: " + tested_content
47
+ ) do |on|
48
+ on(:save) {}
49
+ on(:restore_value) { restore_values(current_user) }
50
+ end
51
+ elsif fire_suspicious_cop && suspicious_spam_cop
52
+ suspicious_spam_cop.call(
53
+ errors: errors,
54
+ error_key: :about,
55
+ suspicious_user: current_user,
56
+ config: suspicious_spam_cop_options,
57
+ justification: "profile: " + tested_content
58
+ ) do |on|
59
+ on(:save) {}
60
+ on(:restore_value) { restore_values(current_user) }
61
+ end
62
+ end
63
+ end
64
+
65
+ # Case the lock cop is there,
66
+ # it will save the user without validation,
67
+ # we should then update the attributes to before
68
+ # state
69
+ def restore_values(user)
70
+ user.about = user.about_was
71
+ user.personal_url = user.personal_url_was
72
+ end
73
+
74
+ def obvious_spam_cop_options
75
+ cop = spam_config.profiles.spam_cop
76
+ return nil unless cop
77
+ cop
78
+ end
79
+
80
+ def scan_context
81
+ {
82
+ validator: "profile",
83
+ is_updating: true,
84
+ date: updated_at,
85
+ current_organization: organization,
86
+ author: self
87
+ }
88
+ end
89
+
90
+ def obvious_spam_cop
91
+ cop_key = obvious_spam_cop_options["handler_name"]
92
+ return nil unless cop_key
93
+ Cops::CopsRepository.instance.strategy(
94
+ cop_key
95
+ )
96
+ end
97
+
98
+ def suspicious_spam_cop_options
99
+ cop = spam_config.profiles.suspicious_cop
100
+ return nil unless cop
101
+ cop
102
+ end
103
+
104
+ def suspicious_spam_cop
105
+ cop_key = suspicious_spam_cop_options["handler_name"]
106
+ return nil unless cop_key
107
+ Cops::CopsRepository.instance.strategy(
108
+ cop_key
109
+ )
110
+ end
111
+
112
+ def spam_config
113
+ @config ||= Config.get_config(organization)
114
+ end
115
+
116
+ def spam_scanners
117
+ spam_config.profiles.scans.map do |s, options|
118
+ options["context"] = scan_context
119
+ [Scans::ScansRepository.instance.strategy(s), options]
120
+ end
121
+ end
122
+
123
+ def spam_ruleset(handler_name = "spam")
124
+ spam_config.profiles.rules.map do |rule_id, rule|
125
+ rule
126
+ end.filter { |r| r["handler_name"] == handler_name }.map do |r|
127
+ r["rules"].symbolize_keys.keys
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ # This holds the decidim-meetings version.
5
+ module SpamSignal
6
+ def self.version
7
+ "0.3.1"
8
+ end
9
+
10
+ def self.decidim_version
11
+ [">= 0.26","<0.28"].freeze
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "spam_signal/admin"
4
+ require_relative "spam_signal/engine"
5
+ require_relative "spam_signal/admin_engine"
6
+
7
+ require_relative "spam_signal/cop_bot"
8
+ require_relative "spam_signal/spam_settings_form_builder"
9
+
10
+ require_relative "spam_signal/validators/profile_spam_validator"
11
+ require_relative "spam_signal/validators/comment_spam_validator"
12
+
13
+ require_relative "spam_signal/extractors/extractor"
14
+ require_relative "spam_signal/extractors/comment_extractor"
15
+ require_relative "spam_signal/extractors/profile_extractor"
16
+
17
+ require_relative "spam_signal/cops/cops_repository"
18
+ require_relative "spam_signal/scans/scans_repository"
19
+
20
+ module Decidim
21
+ # This namespace holds the logic of the `SpamSignal` component. This component
22
+ # allows users to create spam_signal in a participatory space.
23
+ module SpamSignal
24
+ class Error < StandardError; end
25
+ autoload :CopsRepository, "decidim/spam_signal/cops/cops_repository"
26
+ autoload :ScansRepository, "decidim/spam_signal/scans/scans_repository"
27
+
28
+ autoload :WordScanCommand, "decidim/spam_signal/scans/word_scan_command"
29
+ autoload :LockCopCommand, "decidim/spam_signal/cops/lock_cop_command"
30
+ end
31
+ end
@@ -0,0 +1,23 @@
1
+
2
+ now_tag = "\n[#{DateTime.now.strftime("%d/%m/%Y %H:%M")}]"
3
+ Decidim::User.where.not(blocked_at: nil).each do |suspicious_user|
4
+ admin_reporter = CopBot.get(suspicious_user.organization)
5
+ suspicious_comments = Decidim::Comments::Comment.where(author: suspicious_user)
6
+ suspicious_comments.each do |spam|
7
+ moderation = Decidim::Moderation.find_or_create_by!(
8
+ reportable: spam,
9
+ participatory_space: spam.participatory_space
10
+ )
11
+ is_new = moderation.report_count == 0
12
+ moderation.update(reported_content: spam.body[admin_reporter.locale]) if !moderation.reported_content && spam.body[admin_reporter.locale]
13
+ report = Decidim::Report.find_or_create_by!(
14
+ moderation: moderation.reload,
15
+ user: admin_reporter) do |report|
16
+ report.locale = admin_reporter.locale
17
+ report.reason = "spam"
18
+ report.details = "#{now_tag}cascade: #{spam}"
19
+ end
20
+ report.update(details: "#{report.details}#{now_tag}cascade: #{spam}")unless is_new
21
+ moderation.update!(report_count: moderation.report_count + 1, hidden_at: Time.current)
22
+
23
+ end