Skip to content

Commit ff3ec40

Browse files
authored
Merge pull request #1812 from codidact/art/mod-spam-tools
Add mod spammer review tool
2 parents dc5aef4 + 2e0e6f6 commit ff3ec40

12 files changed

Lines changed: 141 additions & 8 deletions

File tree

app/assets/javascripts/moderator.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,13 @@ $(() => {
3838
QPixel.createNotification('danger', `<strong>Failed:</strong> Unexpected status (${req.status})`);
3939
}
4040
});
41+
42+
QPixel.DOM.addSelectorListener('click', '.js-bulk-check', async (ev) => {
43+
const tgt = /** @type {HTMLElement} */(ev.target);
44+
const action = tgt.dataset.check;
45+
const checkboxes = document.querySelectorAll('.js-spammer-form input[type="checkbox"]');
46+
checkboxes.forEach((/** @type {HTMLInputElement} */checkbox) => {
47+
checkbox.checked = action === 'all';
48+
});
49+
});
4150
});

app/assets/stylesheets/forms.scss

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,27 @@ select.form-element {
5858
min-height: (1.2em * $i) + 1.2em;
5959
}
6060
}
61+
62+
.check-group-horizontal {
63+
display: flex;
64+
flex-direction: row;
65+
align-items: center;
66+
67+
label {
68+
padding: 0;
69+
margin: 0;
70+
}
71+
72+
input[type="checkbox"] {
73+
padding: 0;
74+
margin: 0 0 0 0.5em;
75+
}
76+
}
77+
78+
.check-group__right {
79+
justify-content: flex-end;
80+
}
81+
82+
.checkbox__large {
83+
zoom: 1.5;
84+
}

app/assets/stylesheets/utilities.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@
3838
flex-direction: column;
3939
flex-wrap: wrap;
4040

41+
&.space-between {
42+
justify-content: space-between;
43+
}
44+
4145
@media screen and (min-width: $screen-md) {
4246
flex-direction: row;
4347

app/controllers/moderator_controller.rb

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,24 @@ def user_vote_summary
7171
)
7272
end
7373

74+
def spammy_users
75+
script = File.read(Rails.root.join('db/scripts/potential_spam_profiles.sql'))
76+
hours = { 'day' => 24, 'week' => 168, 'month' => 744 }
77+
script = script.gsub('$HOURS', hours[params[:period]]&.to_s || '744')
78+
user_ids = ApplicationRecord.connection.execute(script).to_a.flatten
79+
@users = User.where(id: user_ids).limit(20)
80+
end
81+
82+
def handle_spammy_users
83+
spam = User.where(id: params[:spam_ids])
84+
spam.each do |user|
85+
user.block('Profile spam', length: 10.years, automatic: false)
86+
user.do_soft_delete(current_user)
87+
end
88+
flash[:success] = "#{spam.size} users blocked and deleted."
89+
redirect_to mod_spammers_path
90+
end
91+
7492
private
7593

7694
def set_post

app/jobs/potential_spam_profiles_job.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ class PotentialSpamProfilesJob < ApplicationJob
33

44
def perform
55
sql = File.read(Rails.root.join('db/scripts/potential_spam_profiles.sql'))
6+
sql = sql.gsub('$HOURS', '25')
67
user_ids = ActiveRecord::Base.connection.execute(sql).to_a.flatten
78
users = User.where(id: user_ids)
89

app/models/user.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -390,7 +390,7 @@ def send_welcome_tour_message
390390
'how this site works.', '/tour')
391391
end
392392

393-
def block(reason, length: 180.days)
393+
def block(reason, length: 180.days, automatic: true)
394394
user_email = email
395395
user_ip = [last_sign_in_ip]
396396

@@ -399,10 +399,10 @@ def block(reason, length: 180.days)
399399
end
400400

401401
BlockedItem.create(item_type: 'email', value: user_email, expires: length.from_now,
402-
automatic: true, reason: "#{reason}: #" + id.to_s)
402+
automatic: automatic, reason: "#{reason}: #" + id.to_s)
403403
user_ip.compact.uniq.each do |ip|
404404
BlockedItem.create(item_type: 'ip', value: ip, expires: length.from_now,
405-
automatic: true, reason: "#{reason}: #" + id.to_s)
405+
automatic: automatic, reason: "#{reason}: #" + id.to_s)
406406
end
407407
end
408408

app/views/layouts/_head.html.erb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
<link rel="preconnect" href="https://cdn.jsdelivr.net" />
1919
<link rel="preconnect" href="https://cdnjs.cloudflare.com" />
2020

21-
<%= stylesheet_link_tag "https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@5.11.2/css/all.min.css" %>
21+
<%= stylesheet_link_tag "https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@5.15.4/css/all.min.css" %>
2222
<%= stylesheet_link_tag "https://cdn.jsdelivr.net/npm/@codidact/co-design@latest/dist/codidact.css" %>
2323
<%= stylesheet_link_tag "https://cdn.jsdelivr.net/npm/select2@4.0.12/dist/css/select2.min.css" %>
2424
<%= stylesheet_link_tag "/assets/community/#{@community.host.split('.')[0]}.css" %>

app/views/moderator/index.html.erb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,15 @@
9292
</div>
9393
</div>
9494
</div>
95+
96+
<div class="grid--cell is-4-lg is-6-md is-12-sm">
97+
<div class="widget">
98+
<div class="widget--body">
99+
<i class="fas fa-disease"></i>
100+
<%= link_to 'Spam Profiles', mod_spammers_path %>
101+
</div>
102+
</div>
103+
</div>
95104
</div>
96105

97106
<% chat = SiteSetting['ChatLink'] %>
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<% content_for :title, 'Moderate Spam Profiles' %>
2+
3+
<h1>Spam Profiles</h1>
4+
<p>
5+
These are user accounts which have been automatically identified as potential spam based on the account's
6+
characteristics. Review the list and place a tick against those that are spam, then click Handle to block and delete.
7+
</p>
8+
9+
<div class="flex-row space-between">
10+
<div class="button-list is-gutterless">
11+
<button type="button" class="button is-muted is-outlined js-bulk-check" data-check="all">Tick all</button>
12+
<button type="button" class="button is-muted is-outlined js-bulk-check" data-check="none">Clear all</button>
13+
</div>
14+
<div class="button-list is-gutterless">
15+
<%= link_to '24 hours',
16+
query_url(period: 'day'),
17+
class: "button is-muted is-outlined#{' is-active' if params[:period] == 'day'}" %>
18+
<%= link_to '7 days',
19+
query_url(period: 'week'),
20+
class: "button is-muted is-outlined#{' is-active' if params[:period] == 'week'}" %>
21+
<%= link_to '30 days',
22+
query_url(period: 'month'),
23+
class: "button is-muted is-outlined#{' is-active' if params[:period].blank? || params[:period] == 'month'}" %>
24+
</div>
25+
</div>
26+
27+
<%= form_tag mod_handle_spammers_path, method: :post, class: 'js-spammer-form' do %>
28+
<% @users.each do |user| %>
29+
<div class="widget">
30+
<div class="widget--body">
31+
<%= render 'users/common_card', user: user %>
32+
<pre><%= user.profile_markdown.first(1000) %></pre>
33+
</div>
34+
<div class="widget--footer check-group-horizontal check-group__right">
35+
<%= label_tag "spammer_#{user.id}", 'Spammer?', class: 'form-element' %>
36+
<%= check_box_tag "spam_ids[]", user.id, id: "spammer_#{user.id}", class: 'checkbox__large' %>
37+
</div>
38+
</div>
39+
<% end %>
40+
41+
<% if @users.empty? %>
42+
<p class="has-font-size-display has-text-align-center"><i class="fas fa-virus-slash"></i></p>
43+
<p class="has-text-align-center">Yay! No spammers here.</p>
44+
<% end %>
45+
46+
<div class="actions">
47+
<button type="submit" class="button is-filled is-danger" <%= 'disabled' if @users.empty? %>>Handle</button>
48+
</div>
49+
<% end %>

config/routes.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@
9999
get 'flags', to: 'flags#queue', as: :flag_queue
100100
get 'flags/handled', to: 'flags#handled', as: :handled_flags
101101
get 'flags/escalated', to: 'flags#escalated_queue', as: :escalated_flags
102+
get 'users/spam', to: 'moderator#spammy_users', as: :mod_spammers
103+
post 'users/spam', to: 'moderator#handle_spammy_users', as: :mod_handle_spammers
102104
get 'users/votes/:id', to: 'moderator#user_vote_summary', as: :mod_vote_summary
103105
post 'flags/:id/resolve', to: 'flags#resolve', as: :resolve_flag
104106
post 'flags/:id/escalate', to: 'flags#escalate', as: :escalate_flag

0 commit comments

Comments
 (0)