diff --git a/app/models/dmail.rb b/app/models/dmail.rb index 4e861d5d1..0bfcb7eb0 100644 --- a/app/models/dmail.rb +++ b/app/models/dmail.rb @@ -1,6 +1,11 @@ require 'digest/sha1' class Dmail < ApplicationRecord + # if a person sends spam to more than 10 users within a 24 hour window, automatically ban them for 3 days. + AUTOBAN_THRESHOLD = 10 + AUTOBAN_WINDOW = 24.hours + AUTOBAN_DURATION = 3 + include Rakismet::Model with_options on: :create do @@ -23,6 +28,23 @@ class Dmail < ApplicationRecord rakismet_attrs author: :from_name, author_email: :from_email, content: :title_and_body, user_ip: :creator_ip_addr_str concerning :SpamMethods do + class_methods do + def is_spammer?(user) + return false if user.is_gold? + + spammed_users = sent_by(user).where(is_spam: true).where("created_at > ?", AUTOBAN_WINDOW.ago).distinct.count(:to_id) + spammed_users >= AUTOBAN_THRESHOLD + end + + def ban_spammer(spammer) + spammer.bans.create! do |ban| + ban.banner = User.system + ban.reason = "Spambot." + ban.duration = AUTOBAN_DURATION + end + end + end + def title_and_body "#{title}\n\n#{body}" end @@ -31,9 +53,9 @@ class Dmail < ApplicationRecord creator_ip_addr.to_s end - def spam?(sender = CurrentUser.user) + def spam? return false if Danbooru.config.rakismet_key.blank? - return false if sender.is_gold? + return false if from.is_gold? super() end end @@ -58,7 +80,6 @@ class Dmail < ApplicationRecord def initialize_attributes self.from_id ||= CurrentUser.id self.creator_ip_addr ||= CurrentUser.ip_addr - self.is_spam = spam?(CurrentUser.user) end end @@ -73,6 +94,7 @@ class Dmail < ApplicationRecord # recipient's copy copy = Dmail.new(params) copy.owner_id = copy.to_id + copy.is_spam = copy.spam? copy.save unless copy.to_id == copy.from_id # sender's copy @@ -80,13 +102,15 @@ class Dmail < ApplicationRecord copy.owner_id = copy.from_id copy.is_read = true copy.save + + Dmail.ban_spammer(copy.from) if Dmail.is_spammer?(copy.from) end copy end def create_automated(params) - dmail = Dmail.new(from: Danbooru.config.system_user, **params) + dmail = Dmail.new(from: User.system, **params) dmail.owner = dmail.to dmail.save dmail @@ -119,6 +143,10 @@ class Dmail < ApplicationRecord end module SearchMethods + def sent_by(user) + where("dmails.from_id = ? AND dmails.owner_id != ?", user.id, user.id) + end + def active where("is_deleted = ?", false) end @@ -234,7 +262,7 @@ class Dmail < ApplicationRecord end def is_automated? - from == Danbooru.config.system_user + from == User.system end def filtered? diff --git a/app/models/user.rb b/app/models/user.rb index ca582c23f..be950b7f8 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -316,7 +316,7 @@ class User < ApplicationRecord module ClassMethods def system - Danbooru.config.system_user + User.find_by!(name: Danbooru.config.system_user) end def level_hash @@ -366,7 +366,7 @@ class User < ApplicationRecord def promote_to_admin_if_first_user return if Rails.env.test? - if User.count == 0 + if User.admins.count == 0 self.level = Levels::ADMIN self.can_approve_posts = true self.can_upload_free = true diff --git a/config/danbooru_default_config.rb b/config/danbooru_default_config.rb index ec254f56a..874eef4a6 100644 --- a/config/danbooru_default_config.rb +++ b/config/danbooru_default_config.rb @@ -37,8 +37,10 @@ module Danbooru end # System actions, such as sending automated dmails, will be performed with this account. + # This account will be created automatically if it doesn't exist. It will + # be promoted to Moderator if it isn't already a Moderator. def system_user - User.find_by_name("DanbooruBot") || User.admins.first + "DanbooruBot" end def upload_feedback_topic diff --git a/config/initializers/system_user.rb b/config/initializers/system_user.rb new file mode 100644 index 000000000..2c210facd --- /dev/null +++ b/config/initializers/system_user.rb @@ -0,0 +1,10 @@ +require "securerandom" + +system = User.find_or_create_by!(name: Danbooru.config.system_user) do |user| + user.password = SecureRandom.base64(32) +end + +unless system.is_moderator? + system.level = User::Levels::MODERATOR + system.save +end diff --git a/test/unit/dmail_test.rb b/test/unit/dmail_test.rb index 0d4427cce..7e40ffcd9 100644 --- a/test/unit/dmail_test.rb +++ b/test/unit/dmail_test.rb @@ -3,6 +3,7 @@ require 'test_helper' class DmailTest < ActiveSupport::TestCase context "A dmail" do setup do + User.any_instance.stubs(:validate_sock_puppets).returns(true) @user = FactoryGirl.create(:user) CurrentUser.user = @user CurrentUser.ip_addr = "1.2.3.4" @@ -23,10 +24,24 @@ class DmailTest < ActiveSupport::TestCase should "not validate" do assert_difference("Dmail.count", 2)do - dmail = Dmail.create_split(:to_id => @recipient.id, :title => "My video", :body => "hey Noneeditsonlyme. My webcam see here http://bit.ly/2vTv9Ki") - assert(dmail.is_spam?) + Dmail.create_split(:to_id => @recipient.id, :title => "My video", :body => "hey Noneeditsonlyme. My webcam see here http://bit.ly/2vTv9Ki") + assert(@recipient.dmails.last.is_spam?) end end + + should "autoban spammers after sending spam to N distinct users" do + Dmail.any_instance.expects(:spam?).returns(true) + + users = FactoryGirl.create_list(:user, Dmail::AUTOBAN_THRESHOLD) + users.each do |user| + Dmail.create_split(from: @user, to: user, title: "spam", body: "wonderful spam") + end + + assert_equal(true, Dmail.is_spammer?(@user)) + assert_equal(true, @user.reload.is_banned) + assert_equal(1, @user.bans.count) + assert_match(/Spambot./, @user.bans.last.reason) + end end context "filter" do @@ -177,7 +192,7 @@ class DmailTest < ActiveSupport::TestCase context "that is automated" do setup do @bot = FactoryGirl.create(:user) - Danbooru.config.stubs(:system_user).returns(@bot) + User.stubs(:system).returns(@bot) end should "only create a copy for the recipient" do diff --git a/test/unit/post_disapproval_test.rb b/test/unit/post_disapproval_test.rb index 8b010d8f8..c4ef31399 100644 --- a/test/unit/post_disapproval_test.rb +++ b/test/unit/post_disapproval_test.rb @@ -85,7 +85,7 @@ class PostDisapprovalTest < ActiveSupport::TestCase should "dmail the uploaders" do bot = FactoryGirl.create(:user) - Danbooru.config.stubs(:system_user).returns(bot) + User.stubs(:system).returns(bot) assert_difference(["@uploaders[0].dmails.count", "@uploaders[1].dmails.count"], 1) do PostDisapproval.dmail_messages! diff --git a/test/unit/post_replacement_test.rb b/test/unit/post_replacement_test.rb index e33fdbf4a..3785bafea 100644 --- a/test/unit/post_replacement_test.rb +++ b/test/unit/post_replacement_test.rb @@ -20,7 +20,7 @@ class PostReplacementTest < ActiveSupport::TestCase Delayed::Worker.delay_jobs = true # don't delete the old images right away @system = FactoryGirl.create(:user, created_at: 2.weeks.ago) - Danbooru.config.stubs(:system_user).returns(@system) + User.stubs(:system).returns(@system) @uploader = FactoryGirl.create(:user, created_at: 2.weeks.ago, can_upload_free: true) @replacer = FactoryGirl.create(:user, created_at: 2.weeks.ago, can_approve_posts: true) diff --git a/test/unit/user_test.rb b/test/unit/user_test.rb index 8cd53246e..21efdb64c 100644 --- a/test/unit/user_test.rb +++ b/test/unit/user_test.rb @@ -28,7 +28,7 @@ class UserTest < ActiveSupport::TestCase should "send an automated dmail to the user" do bot = FactoryGirl.create(:user) - Danbooru.config.stubs(:system_user).returns(bot) + User.stubs(:system).returns(bot) assert_difference("Dmail.count", 1) do @user.promote_to!(User::Levels::GOLD)