diff --git a/app/logical/danbooru_logger.rb b/app/logical/danbooru_logger.rb index 4ea882560..2811124cf 100644 --- a/app/logical/danbooru_logger.rb +++ b/app/logical/danbooru_logger.rb @@ -1,4 +1,12 @@ class DanbooruLogger + def self.info(message, params = {}) + Rails.logger.info(message) + + if defined?(::NewRelic) + ::NewRelic::Agent.record_custom_event(:spam, message: message, **params) + end + end + def self.log(exception, expected: false, **params) if expected Rails.logger.info("#{exception.class}: #{exception.message}") diff --git a/app/logical/spam_detector.rb b/app/logical/spam_detector.rb index 6ef629503..d9c7b8ecd 100644 --- a/app/logical/spam_detector.rb +++ b/app/logical/spam_detector.rb @@ -4,7 +4,7 @@ class SpamDetector include Rakismet::Model - attr_accessor :user, :user_ip, :content, :comment_type + attr_accessor :record, :user, :user_ip, :content, :comment_type rakismet_attrs author: proc { user.name }, author_email: proc { user.email }, blog_lang: "en", @@ -24,13 +24,26 @@ class SpamDetector false end - def initialize(record) + def initialize(record, user_ip: nil) case record when Dmail + @record = record @user = record.from @content = record.body @comment_type = "message" - @user_ip = record.creator_ip_addr.to_s + @user_ip = user_ip || record.creator_ip_addr.to_s + when ForumPost + @record = record + @user = record.creator + @content = record.body + @comment_type = record.is_original_post? ? "forum-post" : "reply" + @user_ip = user_ip + when Comment + @record = record + @user = record.creator + @content = record.body + @comment_type = "comment" + @user_ip = user_ip || record.creator_ip_addr.to_s else raise ArgumentError end @@ -39,6 +52,16 @@ class SpamDetector def spam? return false if !SpamDetector.enabled? return false if user.is_gold? - super + + is_spam = super + + if is_spam + DanbooruLogger.info("Spam detected: user_name=#{user.name} comment_type=#{comment_type} content=#{content.dump}", record.as_json) + end + + is_spam + rescue => exception + DanbooruLogger.log(exception) + false end end diff --git a/app/models/comment.rb b/app/models/comment.rb index 68f66365c..4397444a3 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -2,6 +2,7 @@ class Comment < ApplicationRecord include Mentionable validate :validate_creator_is_not_limited, :on => :create + validate :validate_comment_is_not_spam, on: :create validates_presence_of :body, :message => "has no content" belongs_to :post belongs_to_creator @@ -124,6 +125,10 @@ class Comment < ApplicationRecord end end + def validate_comment_is_not_spam + errors[:base] << "Failed to create comment" if SpamDetector.new(self).spam? + end + def update_last_commented_at_on_create Post.where(:id => post_id).update_all(:last_commented_at => created_at) if Comment.where("post_id = ?", post_id).count <= Danbooru.config.comment_threshold && !do_not_bump_post? diff --git a/app/models/forum_post.rb b/app/models/forum_post.rb index 2993e7611..dd7cab394 100644 --- a/app/models/forum_post.rb +++ b/app/models/forum_post.rb @@ -15,6 +15,7 @@ class ForumPost < ApplicationRecord after_destroy :update_topic_updated_at_on_destroy validates_presence_of :body validate :validate_topic_is_unlocked + validate :validate_post_is_not_spam, on: :create validate :topic_is_not_restricted, :on => :create before_destroy :validate_topic_is_unlocked after_save :delete_topic_if_original_post @@ -133,6 +134,10 @@ class ForumPost < ApplicationRecord votes.where(creator_id: user.id, score: score).exists? end + def validate_post_is_not_spam + errors[:base] << "Failed to create forum post" if SpamDetector.new(self, user_ip: CurrentUser.ip_addr).spam? + end + def validate_topic_is_unlocked return if CurrentUser.is_moderator? return if topic.nil? diff --git a/test/unit/spam_detector.rb b/test/unit/spam_detector.rb index 516c64c35..abd98ccba 100644 --- a/test/unit/spam_detector.rb +++ b/test/unit/spam_detector.rb @@ -6,8 +6,8 @@ class SpamDetectorTest < ActiveSupport::TestCase skip "SpamDetector not working: API key not configured, not valid, or akismet is down" if !SpamDetector.working? SpamDetector.stubs(:enabled?).returns(true) - @user = create(:gold_user) - @spammer = create(:user, email: "akismet-guaranteed-spam@example.com") + @user = create(:gold_user, created_at: 1.month.ago) + @spammer = create(:user, created_at: 1.month.ago, email: "akismet-guaranteed-spam@example.com") end context "for dmails" do @@ -26,6 +26,66 @@ class SpamDetectorTest < ActiveSupport::TestCase refute(SpamDetector.new(dmail).spam?) refute(dmail.is_spam?) end + + should "log a message when spam is detected" do + Rails.logger.expects(:info) + Dmail.create_split(from: @spammer, to: @user, title: "spam", body: "wonderful spam", creator_ip_addr: "127.0.0.1") + end + + should "pass messages through if akismet is down" do + Rakismet.expects(:akismet_call).raises(StandardError) + dmail = create(:dmail, from: @spammer, to: @user, owner: @user, title: "spam", body: "wonderful spam", creator_ip_addr: "127.0.0.1") + + refute(SpamDetector.new(dmail).spam?) + end + end + + context "for forum posts" do + setup do + @forum_topic = as(@user) { create(:forum_topic) } + end + + should "detect spam" do + as(@spammer) do + forum_post = build(:forum_post, topic: @forum_topic) + forum_post.validate + + assert(SpamDetector.new(forum_post, user_ip: "127.0.0.1").spam?) + assert(forum_post.invalid?) + assert_equal(["Failed to create forum post"], forum_post.errors.full_messages) + end + end + + should "not detect gold users as spammers" do + as(@user) do + forum_post = create(:forum_post, topic: @forum_topic) + + refute(SpamDetector.new(forum_post).spam?) + assert(forum_post.valid?) + end + end + end + + context "for comments" do + should "detect spam" do + as(@spammer) do + comment = build(:comment) + comment.validate + + assert(SpamDetector.new(comment).spam?) + assert(comment.invalid?) + assert_equal(["Failed to create comment"], comment.errors.full_messages) + end + end + + should "not detect gold users as spammers" do + as(@user) do + comment = create(:comment) + + refute(SpamDetector.new(comment).spam?) + assert(comment.valid?) + end + end end end end