decidim-spam_detection 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE-AGPLv3.txt +661 -0
- data/README.md +75 -0
- data/Rakefile +15 -0
- data/app/commands/decidim/admin/unblock_user.rb +55 -0
- data/app/commands/decidim/admin/unreport_user.rb +53 -0
- data/app/jobs/decidim/spam_detection/mark_users_job.rb +19 -0
- data/app/services/decidim/spam_detection/mark_users_service.rb +63 -0
- data/config/assets.rb +9 -0
- data/config/i18n-tasks.yml +10 -0
- data/config/locales/en.yml +6 -0
- data/lib/decidim/spam_detection/abstract_spam_user_command.rb +65 -0
- data/lib/decidim/spam_detection/admin.rb +10 -0
- data/lib/decidim/spam_detection/admin_engine.rb +27 -0
- data/lib/decidim/spam_detection/api_proxy.rb +53 -0
- data/lib/decidim/spam_detection/block_spam_user_command.rb +37 -0
- data/lib/decidim/spam_detection/command.rb +50 -0
- data/lib/decidim/spam_detection/command_errors.rb +37 -0
- data/lib/decidim/spam_detection/engine.rb +13 -0
- data/lib/decidim/spam_detection/report_spam_user_command.rb +38 -0
- data/lib/decidim/spam_detection/spam_user_command_adapter.rb +33 -0
- data/lib/decidim/spam_detection/test/factories.rb +3 -0
- data/lib/decidim/spam_detection/version.rb +16 -0
- data/lib/decidim/spam_detection.rb +19 -0
- data/lib/tasks/decidim_spam_detection.rake +10 -0
- metadata +81 -0
data/README.md
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
# Decidim::SpamDetection
|
2
|
+
[](https://codecov.io/gh/OpenSourcePolitics/decidim-spam_detection)
|
3
|
+

|
4
|
+

|
5
|
+
|
6
|
+
## Usage
|
7
|
+
|
8
|
+
SpamDetection is a detection bot made by OpenSourcePolitics. It works with a [spam detection service](https://github.com/OpenSourcePolitics/spam_detection)
|
9
|
+
which marks the user with a spam probability score, between 0.7
|
10
|
+
and 0.99 it is probable, and above 0.99 it is very sure.
|
11
|
+
|
12
|
+
By default, the bot does not blocks the user, it only reports them.
|
13
|
+
All reports and blocks are made like regular Decidim ones.
|
14
|
+
|
15
|
+
### Installation
|
16
|
+
|
17
|
+
Add this line to your application's Gemfile:
|
18
|
+
|
19
|
+
```ruby
|
20
|
+
gem "decidim-spam_detection", git: "https://github.com/OpenSourcePolitics/decidim-spam_detection.git"
|
21
|
+
```
|
22
|
+
|
23
|
+
And then execute:
|
24
|
+
|
25
|
+
```bash
|
26
|
+
bundle exec rake decidim:spam_detection:mark_users
|
27
|
+
```
|
28
|
+
|
29
|
+
if you are using sidekiq scheduler you can use the following configuration:
|
30
|
+
```
|
31
|
+
:queues:
|
32
|
+
- user_report
|
33
|
+
- block_user
|
34
|
+
- scheduled
|
35
|
+
|
36
|
+
:schedule:
|
37
|
+
DetectSpamUsers:
|
38
|
+
cron: '0 0 8 * * *' # Run at 08:00
|
39
|
+
class: Decidim::SpamDetection::MarkUsersJob
|
40
|
+
queue: scheduled
|
41
|
+
```
|
42
|
+
|
43
|
+
### Further configuration
|
44
|
+
list of env var, default value and their usage:
|
45
|
+
```
|
46
|
+
SPAM_DETECTION_API_AUTH_TOKEN
|
47
|
+
default_value: dummy
|
48
|
+
usage: Token auth for authentication used by external service, ask us for more details
|
49
|
+
SPAM_DETECTION_API_URL
|
50
|
+
default_value: "http://localhost:8080/api"
|
51
|
+
usage: URL of the external service
|
52
|
+
SPAM_DETECTION_NAME
|
53
|
+
default_value: "spam detection bot"
|
54
|
+
usage: Name used by the spam detection bot
|
55
|
+
SPAM_DETECTION_NICKNAME
|
56
|
+
default_value: "Spam_detection_bot"
|
57
|
+
usage: Nickname used by the spam detection bot
|
58
|
+
SPAM_DETECTION_EMAIL
|
59
|
+
default_value: "spam_detection_bot@opensourcepolitcs.eu"
|
60
|
+
usage: Email used by the spam detection bot
|
61
|
+
PERFORM_BLOCK_USER
|
62
|
+
default_value: false
|
63
|
+
usage: Determine if the bot can perform blocking, default mode is just report
|
64
|
+
```
|
65
|
+
|
66
|
+
## API usage
|
67
|
+
We can provide the detection service, please check us out at [contact@opensourcepolitics.eu](mailto:contact@opensourcepolitics.eu)
|
68
|
+
|
69
|
+
## Contributing
|
70
|
+
|
71
|
+
See [Decidim](https://github.com/decidim/decidim).
|
72
|
+
|
73
|
+
## License
|
74
|
+
|
75
|
+
This engine is distributed under the GNU AFFERO GENERAL PUBLIC LICENSE.
|
data/Rakefile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "decidim/dev/common_rake"
|
4
|
+
|
5
|
+
desc "Generates a dummy app for testing"
|
6
|
+
task test_app: "decidim:generate_external_test_app"
|
7
|
+
|
8
|
+
desc "Generates a development app."
|
9
|
+
task development_app: "decidim:generate_external_development_app"
|
10
|
+
|
11
|
+
task :push_tag_and_release do
|
12
|
+
system("git tag v#{Decidim::SpamDetection.version}")
|
13
|
+
system("git push --tags")
|
14
|
+
system("gh release create v#{Decidim::SpamDetection.version}")
|
15
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Decidim
|
4
|
+
module Admin
|
5
|
+
class UnblockUser < Rectify::Command
|
6
|
+
# Public: Initializes the command.
|
7
|
+
#
|
8
|
+
# blocked_user - the user that is unblocked
|
9
|
+
# current_user - the user performing the action
|
10
|
+
def initialize(blocked_user, current_user)
|
11
|
+
@blocked_user = blocked_user
|
12
|
+
@current_user = current_user
|
13
|
+
end
|
14
|
+
|
15
|
+
# Executes the command. Broadcasts these events:
|
16
|
+
#
|
17
|
+
# - :ok when everything is valid, together with the resource.
|
18
|
+
# - :invalid if the resource is not reported
|
19
|
+
#
|
20
|
+
# Returns nothing.
|
21
|
+
def call
|
22
|
+
return broadcast(:invalid) unless @blocked_user.blocked?
|
23
|
+
|
24
|
+
unblock!
|
25
|
+
add_spam_detection_metadata!
|
26
|
+
broadcast(:ok, @blocked_user)
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def unblock!
|
32
|
+
Decidim.traceability.perform_action!(
|
33
|
+
"unblock",
|
34
|
+
@blocked_user,
|
35
|
+
@current_user,
|
36
|
+
extra: {
|
37
|
+
reportable_type: @blocked_user.class.name
|
38
|
+
}
|
39
|
+
) do
|
40
|
+
@blocked_user.blocked = false
|
41
|
+
@blocked_user.blocked_at = nil
|
42
|
+
@blocked_user.block_id = nil
|
43
|
+
@blocked_user.name = @blocked_user.user_name
|
44
|
+
@blocked_user.save!
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def add_spam_detection_metadata!
|
49
|
+
return if @blocked_user.extended_data.dig("spam_detection", "blocked_at").blank?
|
50
|
+
|
51
|
+
@blocked_user.update!(extended_data: @blocked_user.extended_data.dup.deep_merge("spam_detection" => { "unblocked_at": Time.current }))
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Decidim
|
4
|
+
module Admin
|
5
|
+
class UnreportUser < Rectify::Command
|
6
|
+
# Public: Initializes the command.
|
7
|
+
#
|
8
|
+
# reportable - A Decidim::User - The user reported
|
9
|
+
# current_user - the user performing the action
|
10
|
+
def initialize(reportable, current_user)
|
11
|
+
@reportable = reportable
|
12
|
+
@current_user = current_user
|
13
|
+
end
|
14
|
+
|
15
|
+
# Executes the command. Broadcasts these events:
|
16
|
+
#
|
17
|
+
# - :ok when everything is valid, together with the resource.
|
18
|
+
# - :invalid if the resource is not reported
|
19
|
+
#
|
20
|
+
# Returns nothing.
|
21
|
+
def call
|
22
|
+
return broadcast(:invalid) unless @reportable.reported?
|
23
|
+
|
24
|
+
unreport!
|
25
|
+
add_spam_detection_metadata!
|
26
|
+
broadcast(:ok, @reportable)
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def unreport!
|
32
|
+
Decidim.traceability.perform_action!(
|
33
|
+
"unreport",
|
34
|
+
@reportable.user_moderation,
|
35
|
+
@current_user,
|
36
|
+
extra: {
|
37
|
+
reportable_type: @reportable.class.name,
|
38
|
+
username: @reportable.name,
|
39
|
+
user_id: @reportable.id
|
40
|
+
}
|
41
|
+
) do
|
42
|
+
@reportable.user_moderation.destroy!
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def add_spam_detection_metadata!
|
47
|
+
return if @reportable.extended_data.dig("spam_detection", "reported_at").blank?
|
48
|
+
|
49
|
+
@reportable.update!(extended_data: @reportable.extended_data.dup.deep_merge("spam_detection" => { "unreported_at": Time.current }))
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Decidim
|
4
|
+
module SpamDetection
|
5
|
+
class MarkUsersJob < ApplicationJob
|
6
|
+
queue_as :default
|
7
|
+
|
8
|
+
def perform
|
9
|
+
mark_users_service.call
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def mark_users_service
|
15
|
+
@mark_users_service ||= Decidim::SpamDetection::MarkUsersService
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "uri"
|
4
|
+
require "net/http"
|
5
|
+
|
6
|
+
module Decidim
|
7
|
+
module SpamDetection
|
8
|
+
class MarkUsersService
|
9
|
+
PUBLICY_SEARCHABLE_COLUMNS = [
|
10
|
+
:id,
|
11
|
+
:decidim_organization_id,
|
12
|
+
:sign_in_count,
|
13
|
+
:personal_url,
|
14
|
+
:about,
|
15
|
+
:avatar,
|
16
|
+
:extended_data,
|
17
|
+
:followers_count,
|
18
|
+
:following_count,
|
19
|
+
:invitations_count,
|
20
|
+
:failed_attempts,
|
21
|
+
:admin
|
22
|
+
].freeze
|
23
|
+
|
24
|
+
def initialize
|
25
|
+
@users = Decidim::User.left_outer_joins(:user_moderation)
|
26
|
+
.where(decidim_user_moderations: { decidim_user_id: nil })
|
27
|
+
.where(admin: false, blocked: false, deleted_at: nil)
|
28
|
+
.where("(extended_data #> '{spam_detection, unreported_at}') is null")
|
29
|
+
.where("(extended_data #> '{spam_detection, unblocked_at}') is null")
|
30
|
+
@results = []
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.call
|
34
|
+
new.ask_and_mark
|
35
|
+
end
|
36
|
+
|
37
|
+
def ask_and_mark
|
38
|
+
spam_probability_array = Decidim::SpamDetection::ApiProxy.request(cleaned_users)
|
39
|
+
|
40
|
+
mark_spam_users(merge_response_with_users(spam_probability_array))
|
41
|
+
end
|
42
|
+
|
43
|
+
def mark_spam_users(probability_array)
|
44
|
+
probability_array.each do |probability_hash|
|
45
|
+
@results << Decidim::SpamDetection::SpamUserCommandAdapter.call(probability_hash).result
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def cleaned_users
|
50
|
+
@cleaned_users ||= @users.select(PUBLICY_SEARCHABLE_COLUMNS)
|
51
|
+
.map { |u| u.serializable_hash(force_except: true) }
|
52
|
+
end
|
53
|
+
|
54
|
+
def merge_response_with_users(response)
|
55
|
+
response.map { |resp| resp.merge("original_user" => @users.find(resp["id"])) }
|
56
|
+
end
|
57
|
+
|
58
|
+
def status
|
59
|
+
@results.tally
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
data/config/assets.rb
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
base_path = File.expand_path("..", __dir__)
|
4
|
+
|
5
|
+
Decidim::Webpacker.register_path("#{base_path}/app/packs")
|
6
|
+
Decidim::Webpacker.register_entrypoints(
|
7
|
+
decidim_spam_detection: "#{base_path}/app/packs/entrypoints/decidim_spam_detection.js"
|
8
|
+
)
|
9
|
+
Decidim::Webpacker.register_stylesheet_import("stylesheets/decidim/spam_detection/spam_detection")
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "uri"
|
4
|
+
require "net/http"
|
5
|
+
|
6
|
+
module Decidim
|
7
|
+
module SpamDetection
|
8
|
+
class AbstractSpamUserCommand
|
9
|
+
SPAM_USER = {
|
10
|
+
name: ENV.fetch("SPAM_DETECTION_NAME", "spam detection bot"),
|
11
|
+
nickname: ENV.fetch("SPAM_DETECTION_NICKNAME", "Spam_detection_bot"),
|
12
|
+
email: ENV.fetch("SPAM_DETECTION_EMAIL", "spam_detection_bot@opensourcepolitcs.eu")
|
13
|
+
}.freeze
|
14
|
+
|
15
|
+
include Decidim::FormFactory
|
16
|
+
|
17
|
+
def initialize(user, probability)
|
18
|
+
@user = user
|
19
|
+
@probability = probability
|
20
|
+
@moderator = moderation_user
|
21
|
+
end
|
22
|
+
|
23
|
+
def call
|
24
|
+
raise NotImplementedError
|
25
|
+
end
|
26
|
+
|
27
|
+
def moderation_user
|
28
|
+
moderation_admin_params = {
|
29
|
+
name: SPAM_USER[:name],
|
30
|
+
nickname: SPAM_USER[:nickname],
|
31
|
+
email: SPAM_USER[:email],
|
32
|
+
admin: true,
|
33
|
+
organization: @user.organization
|
34
|
+
}
|
35
|
+
|
36
|
+
moderation_admin = Decidim::User.find_by(moderation_admin_params)
|
37
|
+
|
38
|
+
return moderation_admin unless moderation_admin.nil?
|
39
|
+
|
40
|
+
create_moderation_admin(moderation_admin_params)
|
41
|
+
end
|
42
|
+
|
43
|
+
def create_moderation_admin(params)
|
44
|
+
password = ::Devise.friendly_token(::Devise.password_length.last)
|
45
|
+
additional_params = {
|
46
|
+
password: password,
|
47
|
+
password_confirmation: password,
|
48
|
+
tos_agreement: true,
|
49
|
+
email_on_notification: false,
|
50
|
+
email_on_moderations: false
|
51
|
+
}
|
52
|
+
moderation_admin = Decidim::User.new(params.merge(additional_params))
|
53
|
+
moderation_admin.skip_confirmation!
|
54
|
+
moderation_admin.save
|
55
|
+
moderation_admin
|
56
|
+
end
|
57
|
+
|
58
|
+
def add_spam_detection_metadata!(metadata)
|
59
|
+
@user.update!(extended_data: @user.extended_data
|
60
|
+
.dup
|
61
|
+
.deep_merge("spam_detection" => metadata))
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Decidim
|
4
|
+
module SpamDetection
|
5
|
+
# This is the engine that runs on the public interface of `SpamDetection`.
|
6
|
+
class AdminEngine < ::Rails::Engine
|
7
|
+
isolate_namespace Decidim::SpamDetection::Admin
|
8
|
+
|
9
|
+
paths["db/migrate"] = nil
|
10
|
+
paths["lib/tasks"] = nil
|
11
|
+
|
12
|
+
routes do
|
13
|
+
# Add admin engine routes here
|
14
|
+
# resources :spam_detection do
|
15
|
+
# collection do
|
16
|
+
# resources :exports, only: [:create]
|
17
|
+
# end
|
18
|
+
# end
|
19
|
+
# root to: "spam_detection#index"
|
20
|
+
end
|
21
|
+
|
22
|
+
def load_seed
|
23
|
+
nil
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "uri"
|
4
|
+
require "net/http"
|
5
|
+
|
6
|
+
module Decidim
|
7
|
+
module SpamDetection
|
8
|
+
class ApiProxy
|
9
|
+
URL = URI(ENV.fetch("SPAM_DETECTION_API_URL", "http://localhost:8080/api"))
|
10
|
+
AUTH_TOKEN = ENV.fetch("SPAM_DETECTION_API_AUTH_TOKEN", "dummy")
|
11
|
+
|
12
|
+
def initialize(data_array, batch_size)
|
13
|
+
@data_array = data_array
|
14
|
+
@batch_size = batch_size
|
15
|
+
@retries = [3, 5, 10]
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.request(data_array, batch_size = 1000)
|
19
|
+
new(data_array, batch_size).send_request_in_batch
|
20
|
+
end
|
21
|
+
|
22
|
+
def send_request_in_batch
|
23
|
+
responses = []
|
24
|
+
@data_array.each_slice(@batch_size) do |subdata_array|
|
25
|
+
responses << JSON.parse(send_request_to_api(subdata_array))
|
26
|
+
end
|
27
|
+
|
28
|
+
responses.flatten
|
29
|
+
end
|
30
|
+
|
31
|
+
def send_request_to_api(data)
|
32
|
+
http = Net::HTTP.new(URL.host, URL.port)
|
33
|
+
request = Net::HTTP::Post.new(URL)
|
34
|
+
request["Content-Type"] = "application/json"
|
35
|
+
request["AUTH_TOKEN"] = AUTH_TOKEN
|
36
|
+
request.body = JSON.dump(data)
|
37
|
+
http.use_ssl = true if self.class.use_ssl?(URL)
|
38
|
+
response = http.request(request)
|
39
|
+
response.read_body
|
40
|
+
rescue Net::ReadTimeout
|
41
|
+
raise Net::ReadTimeout if @retries.empty?
|
42
|
+
|
43
|
+
sleep @retries.first
|
44
|
+
@retries.shift
|
45
|
+
retry
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.use_ssl?(url)
|
49
|
+
url.scheme == "https"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "uri"
|
4
|
+
require "net/http"
|
5
|
+
|
6
|
+
module Decidim
|
7
|
+
module SpamDetection
|
8
|
+
class BlockSpamUserCommand < Decidim::SpamDetection::AbstractSpamUserCommand
|
9
|
+
prepend Decidim::SpamDetection::Command
|
10
|
+
|
11
|
+
def call
|
12
|
+
form = form(Decidim::Admin::BlockUserForm).from_params(
|
13
|
+
justification: "The user was blocked because of a high spam probability by Decidim spam detection bot"
|
14
|
+
)
|
15
|
+
|
16
|
+
moderator = @moderator
|
17
|
+
user = @user
|
18
|
+
|
19
|
+
form.define_singleton_method(:user) { user }
|
20
|
+
form.define_singleton_method(:current_user) { moderator }
|
21
|
+
form.define_singleton_method(:blocking_user) { moderator }
|
22
|
+
|
23
|
+
Decidim::Admin::BlockUser.call(form)
|
24
|
+
|
25
|
+
add_spam_detection_metadata!({
|
26
|
+
"blocked_at" => Time.current,
|
27
|
+
"spam_probability" => @probability
|
28
|
+
})
|
29
|
+
|
30
|
+
@user.create_user_moderation
|
31
|
+
Rails.logger.info("User with id #{@user["id"]} was blocked for spam")
|
32
|
+
|
33
|
+
:ok
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "decidim/spam_detection/command_errors"
|
4
|
+
|
5
|
+
module Decidim
|
6
|
+
module SpamDetection
|
7
|
+
module Command
|
8
|
+
attr_reader :result
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
def call(*args, **kwargs)
|
12
|
+
new(*args, **kwargs).call
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.prepended(base)
|
17
|
+
base.extend ClassMethods
|
18
|
+
end
|
19
|
+
|
20
|
+
def call
|
21
|
+
raise NotImplementedError unless defined?(super)
|
22
|
+
|
23
|
+
@called = true
|
24
|
+
@result = super
|
25
|
+
|
26
|
+
self
|
27
|
+
end
|
28
|
+
|
29
|
+
def success?
|
30
|
+
called? && !failure?
|
31
|
+
end
|
32
|
+
|
33
|
+
def failure?
|
34
|
+
called? && errors.any?
|
35
|
+
end
|
36
|
+
|
37
|
+
def errors
|
38
|
+
return super if defined?(super)
|
39
|
+
|
40
|
+
@errors ||= Decidim::SpamDetection::CommandErrors.new
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def called?
|
46
|
+
@called ||= false
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Decidim
|
4
|
+
module SpamDetection
|
5
|
+
class CommandErrors < Hash
|
6
|
+
def add(key, value, _opts = {})
|
7
|
+
self[key] ||= []
|
8
|
+
self[key] << value
|
9
|
+
self[key].uniq!
|
10
|
+
end
|
11
|
+
|
12
|
+
def add_multiple_errors(errors_hash)
|
13
|
+
errors_hash.each do |key, values|
|
14
|
+
values.each { |value| add key, value }
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def each
|
19
|
+
each_key do |field|
|
20
|
+
self[field].each { |message| yield field, message }
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def full_messages
|
25
|
+
map { |attribute, message| full_message(attribute, message) }
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def full_message(attribute, message)
|
31
|
+
return message if attribute == :base
|
32
|
+
|
33
|
+
"#{attribute.to_s.tr(".", "_").capitalize} #{message}"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rails"
|
4
|
+
require "decidim/core"
|
5
|
+
|
6
|
+
module Decidim
|
7
|
+
module SpamDetection
|
8
|
+
# This is the engine that runs on the public interface of spam_detection.
|
9
|
+
class Engine < ::Rails::Engine
|
10
|
+
isolate_namespace Decidim::SpamDetection
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "uri"
|
4
|
+
require "net/http"
|
5
|
+
|
6
|
+
module Decidim
|
7
|
+
module SpamDetection
|
8
|
+
class ReportSpamUserCommand < Decidim::SpamDetection::AbstractSpamUserCommand
|
9
|
+
prepend Decidim::SpamDetection::Command
|
10
|
+
|
11
|
+
def call
|
12
|
+
form = form(Decidim::ReportForm).from_params(
|
13
|
+
reason: "spam",
|
14
|
+
details: "The user was marked as spam by Decidim spam detection bot"
|
15
|
+
)
|
16
|
+
|
17
|
+
current_organization = @user.organization
|
18
|
+
moderator = @moderator
|
19
|
+
user = @user
|
20
|
+
|
21
|
+
report = Decidim::CreateUserReport.new(form, user, moderator)
|
22
|
+
report.define_singleton_method(:current_organization) { current_organization }
|
23
|
+
report.define_singleton_method(:current_user) { moderator }
|
24
|
+
report.define_singleton_method(:reportable) { user }
|
25
|
+
report.call
|
26
|
+
|
27
|
+
add_spam_detection_metadata!({
|
28
|
+
"reported_at" => Time.current,
|
29
|
+
"spam_probability" => @probability
|
30
|
+
})
|
31
|
+
|
32
|
+
Rails.logger.info("User with id #{user.id} was reported for spam")
|
33
|
+
|
34
|
+
:ok
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Decidim
|
4
|
+
module SpamDetection
|
5
|
+
class SpamUserCommandAdapter
|
6
|
+
prepend Decidim::SpamDetection::Command
|
7
|
+
SPAM_LEVEL = { very_sure: 0.99, probable: 0.7 }.freeze
|
8
|
+
|
9
|
+
def self.perform_block_user?
|
10
|
+
ENV.fetch("PERFORM_BLOCK_USER", false)
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(probability_hash)
|
14
|
+
@probability = probability_hash["spam_probability"]
|
15
|
+
@user = probability_hash["original_user"]
|
16
|
+
end
|
17
|
+
|
18
|
+
def call
|
19
|
+
if @probability > SPAM_LEVEL[:very_sure] && self.class.perform_block_user?
|
20
|
+
Decidim::SpamDetection::BlockSpamUserCommand.call(@user, @probability)
|
21
|
+
|
22
|
+
:blocked_user
|
23
|
+
elsif @probability > SPAM_LEVEL[:probable]
|
24
|
+
Decidim::SpamDetection::ReportSpamUserCommand.call(@user, @probability)
|
25
|
+
|
26
|
+
:reported_user
|
27
|
+
else
|
28
|
+
:nothing
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|