diff --git a/.env.example b/.env.example index b9a00a7..d1a9246 100644 --- a/.env.example +++ b/.env.example @@ -6,4 +6,5 @@ SPOKEN_DB='13thJune' SECRET_KEY='' VIDEO_PATH='' DEBUG=True -TEMPLATE_DEBUG=True \ No newline at end of file +TEMPLATE_DEBUG=True +SPAM_LOG_FILE='' \ No newline at end of file diff --git a/forums/logs/spam_detection.log b/forums/logs/spam_detection.log new file mode 100644 index 0000000..e69de29 diff --git a/forums/settings.py b/forums/settings.py index 6c549d2..61230db 100644 --- a/forums/settings.py +++ b/forums/settings.py @@ -17,6 +17,11 @@ # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +SPAM_LOG_FILE = os.getenv("SPAM_LOG_FILE") + +# reCAPTCHA Settings +RECAPTCHA_SITE_KEY = os.getenv("RECAPTCHA_SITE_KEY") +RECAPTCHA_SECRET_KEY = os.getenv("RECAPTCHA_SECRET_KEY") # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/ @@ -108,8 +113,8 @@ 'ENGINE': 'django.db.backends.mysql', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'. 'NAME': os.getenv("SPOKEN_DB"), # Or path to database file if using sqlite3. # The following settings are not used with sqlite3: - 'USER': os.getenv("DB_USER"), - 'PASSWORD': os.getenv("DB_PASSWORD"), + 'USER': os.getenv("SPOKEN_DB_USER"), + 'PASSWORD': os.getenv("SPOKEN_DB_PASSWORD"), # Empty for localhost through domain sockets or '127.0.0.1' for localhost through TCP. 'HOST': '', 'PORT': '', # Set to empty string for default. @@ -150,7 +155,7 @@ LANGUAGE_CODE = 'en-us' -TIME_ZONE = 'Asia/Calcutta' +TIME_ZONE = 'Asia/Kolkata' USE_I18N = True @@ -267,29 +272,54 @@ except ImportError: pass - LOGGING = { 'version': 1, 'disable_existing_loggers': False, + 'filters': { 'require_debug_false': { '()': 'django.utils.log.RequireDebugFalse' } }, + + 'formatters': { + 'verbose': { + 'format': '[{asctime}] {levelname} {name}: {message}', + 'style': '{', + }, + }, + 'handlers': { 'mail_admins': { 'level': 'ERROR', 'filters': ['require_debug_false'], 'class': 'django.utils.log.AdminEmailHandler' - } + }, + + # New handler for spam detection + 'spam_file': { + 'level': 'INFO', + 'class': 'logging.handlers.RotatingFileHandler', + 'filename': SPAM_LOG_FILE, # create logs/ dir + 'maxBytes': 1024 * 1024 * 5, # 5 MB + 'backupCount': 5, # keep 5 old logs + 'formatter': 'verbose', + }, }, + 'loggers': { 'django.request': { 'handlers': ['mail_admins'], 'level': 'ERROR', 'propagate': True, }, + + # New dedicated logger + 'spam_detection': { + 'handlers': ['spam_file'], + 'level': 'INFO', + 'propagate': False, + }, } } - VIDEO_PATH = os.getenv("VIDEO_PATH") \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 08937ec..27abac0 100755 --- a/requirements.txt +++ b/requirements.txt @@ -15,3 +15,4 @@ django-debug-toolbar==1.4 python-dotenv==0.10.3 nltk==3.5 sklearn==0.0 +requests>=2.25.0 diff --git a/seed_spam_rules.py b/seed_spam_rules.py new file mode 100644 index 0000000..3e393b0 --- /dev/null +++ b/seed_spam_rules.py @@ -0,0 +1,116 @@ +# Script to seed the database with predefined spam rules +import os +import django + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "forums.settings") +django.setup() + +from django.db.models import Q +from website.models import SpamRule + + + +def seed_spam_rules(): + rules = { + # Certification/Exam dump patterns + "Certification/Exam Spam": { + "score": 30, + "type": SpamRule.KEYWORD, + "patterns": [ + r"exam\s+dumps?", r"braindumps?", r"practice\s+test", + r"certification\s+exam", r"test\s+preparation", + r"exam\s+questions?", r"study\s+guides?", + r"pdf\s+\+\s+testing\s+engine", r"testing\s+engine", + r"exam\s+prep", r"mock\s+exam", r"real\s+exam", + r"dumps\s+pdf", r"braindump" + ], + }, + + # Promotional spam + "Promotional Spam": { + "score": 25, + "type": SpamRule.KEYWORD, + "patterns": [ + r"click\s+here", r"join\s+now", r"limited\s+time", + r"discount", r"coupon\s+code", r"20%\s+off", + r"free\s+download", r"get\s+certified", + r"unlock\s+your\s+career", r"master\s+the", + r"boost\s+your\s+career", r"cert20", + r"at\s+checkout", r"special\s+offer", + ], + }, + + # Suspicious domains + "Suspicious Domain": { + "score": 35, + "type": SpamRule.DOMAIN, + "patterns": [ + r"dumpscafe\.com", r"certsout\.com", r"mycertshub\.com", + r"vmexam\.com", r"kissnutra\.com", r"dumps.*\.com", + r"cert.*\.com", r"exam.*\.com", + ], + }, + + # Generic business language + "Business/Career Spam": { + "score": 15, + "type": SpamRule.KEYWORD, + "patterns": [ + r"attests\s+to\s+your\s+proficiency", + r"esteemed\s+(?:accreditation|certification|credential)", + r"valuable\s+asset\s+to\s+companies", + r"demonstrates\s+your\s+ability", + r"comprehensive\s+study\s+(?:tools|materials)", + r"interactive\s+practice\s+tests", + r"real\s+exam\s+questions", + r"actual\s+exam\s+questions", + r"validated\s+by\s+.*certification", + r"urgently\s+need\s+experts", + ], + }, + + # Gaming content + "Gaming Spam": { + "score": 20, + "type": SpamRule.KEYWORD, + "patterns": [ + r"spacebar\s+clicker", r"clicker\s+game", + r"addictive\s+game", r"upgrades\s+available", + r"instant\s+rewards", + ], + }, + + # Health/Supplement spam + "Health Spam": { + "score": 22, + "type": SpamRule.KEYWORD, + "patterns": [ + r"vitalit[äa]t", r"nahrungserg[äa]nzungsmittel", + r"libido", r"fruchtbarkeit", r"energie", + r"hormonelle\s+balance", r"perforan", + ], + }, + } + + inserted, skipped = 0, 0 + for note, config in rules.items(): + for pattern in config["patterns"]: + exists = SpamRule.objects.filter( + Q(pattern=pattern) & Q(type=config["type"]) + ).exists() + if not exists: + SpamRule.objects.create( + type=config["type"], + pattern=pattern, + score=config["score"], + notes=note, + ) + inserted += 1 + else: + skipped += 1 + + print(f"✅ Inserted {inserted} new rules, skipped {skipped} existing ones.") + + +# Run it +seed_spam_rules() diff --git a/static/website/templates/index.html b/static/website/templates/index.html index 71554a1..c6e1ae0 100755 --- a/static/website/templates/index.html +++ b/static/website/templates/index.html @@ -93,6 +93,7 @@

Answers

@@ -114,6 +115,7 @@

Answers

{% for question in questions %} + {% if 'test-' not in question.category %} @@ -177,6 +179,7 @@

Answers

+ {% endif %} {% endfor %} @@ -279,7 +282,45 @@

Answers

- +
+ + + + + + + + + + + + + + + {% for question in spam_questions %} + + + + + + + + + + + {% empty %} + + {% endfor %} + +
FOSSTutorialMinSecQuestionDateUserActions
{{ question.category|truncatechars:12 }}{{ question.tutorial|truncatechars:12 }}{{ question.minute_range }}{{ question.second_range }} + + {{ question.title|truncatechars:40 }} + + {{ question.date_created|date:"d-m-y" }}{{ question.user|truncatechars:10 }} + + +
No spam questions pending approval.
+
{% endblock %} @@ -324,6 +365,41 @@

Answers

} e.preventDefault(); }); + + // Spam approval handlers + $('.spam-approve').click(function() { + var qid = $(this).data('qid'); + var row = $('#spam-row-' + qid); + + $.post('/ajax-spam-approve/', { + question_id: qid, + csrfmiddlewaretoken: $('[name=csrfmiddlewaretoken]').val() + }, function(response) { + if(response.success) { + row.fadeOut(function() { $(this).remove(); }); + alert('Question approved!'); + } else { + alert('Error approving question: ' + (response.error || 'Unknown error')); + } + }, 'json'); + }); + + $('.spam-reject').click(function() { + var qid = $(this).data('qid'); + var row = $('#spam-row-' + qid); + + $.post('/ajax-spam-reject/', { + question_id: qid, + csrfmiddlewaretoken: $('[name=csrfmiddlewaretoken]').val() + }, function(response) { + if(response.success) { + row.fadeOut(function() { $(this).remove(); }); + alert('Question rejected as spam!'); + } else { + alert('Error rejecting question: ' + (response.error || 'Unknown error')); + } + }, 'json'); + }); }); $('span').tooltip(); diff --git a/static/website/templates/new-question.html b/static/website/templates/new-question.html index c088447..78dea29 100755 --- a/static/website/templates/new-question.html +++ b/static/website/templates/new-question.html @@ -54,6 +54,13 @@

{% render_field form.body class+="form-control" %} + + + {% if require_recaptcha %} +
+
+
+ {% endif %} @@ -76,6 +83,7 @@

+