Initial commit

This commit is contained in:
zv
2025-10-17 12:25:12 +02:00
commit 7f6e59eb0e
17 changed files with 185219 additions and 0 deletions

124
README.md Normal file
View File

@@ -0,0 +1,124 @@
# 🛡️ AntiScam Pro
[![Python](https://img.shields.io/badge/Python-3.10%2B-blue.svg?logo=python&logoColor=white)](https://www.python.org/)
[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
[![Open Source](https://img.shields.io/badge/Open%20Source-%E2%9D%A4-lightgrey.svg)](https://github.com/zvspany/antiscam-pro)
[![Contributions Welcome](https://img.shields.io/badge/Contributions-Welcome-blueviolet.svg)](https://github.com/zvspany/antiscam-pro/pulls)
---
**AntiScam Pro** is a modern, open-source security toolkit designed to detect **phishing, scam, and spam** in messages, phone numbers, and URLs.
It combines **AI-powered analysis (BERT)**, **heuristic detection**, and **VirusTotal integration** to deliver precise, transparent, and explainable results.
---
## ✨ Key Features
- 🤖 **AI-powered message classification** fine-tuned BERT model detects phishing or spam text.
- 🔗 **Smart URL inspection** resolves redirects, short links, and checks domains via VirusTotal.
- 📞 **Phone number validation** detects known or suspicious scam numbers.
- 🌐 **REST API + Web UI** interact easily from a browser or programmatically.
- 📊 **Session & global statistics** track analysis metrics over time.
- 🔒 **Privacy-first design** no data is stored or sent externally.
---
## ⚙️ Installation
```bash
git clone https://github.com/zvspany/antiscam-pro.git
cd antiscam-pro
pip install -r requirements.txt
```
If you want to enable **VirusTotal integration**, create `.env` file in the project root:
```
VIRUSTOTAL_API_KEY=your_api_key_here
FLASK_SECRET_KEY=change_this_secret
```
Then run the app:
```
python app.py
```
Access the dashboard at:
👉 http://127.0.0.1:5000
---
## 💬 Usage
**You can use AntiScam Pro either from the browser UI or the API endpoints.**
---
## 🧠 How It Works
AntiScam Pro applies **multi-layered analysis** for high accuracy and interpretability:
**1. Input Validation** detects whether the user provided a message, URL, or number.
**2. Heuristic Filters** scans for known scam keywords, shorteners, or flagged domains.
**3. AI Classification** BERT model predicts whether text is phishing, spam, or safe.
**4. External Intelligence** optional VirusTotal API check for additional reputation data.
**5. Decision Fusion** merges signals into a single final verdict.
This architecture ensures both **speed** and **transparency**, making it suitable for research and production use.
---
## 🧰 Tech Stack
- **🐍 Python 3** + **Flask** lightweight and fast backend
- **🤗 HuggingFace Transformers (BERT)** machine learning for text analysis
- **🌍 Requests** robust HTTP handling
- **🧩 VirusTotal API** optional link intelligence
- **⚙️ Regex** + **heuristic rules** simple but effective early filtering
---
## 🪶 Design Philosophy
AntiScam Pro is built around four core principles:
**🕵️‍♂️ Transparency** detection logic is fully open and auditable
**⚡ Simplicity** lightweight, minimal dependencies
**🔐 Privacy** no user tracking, no telemetry, no cloud sync
**🧩 Extensibility** easy to add models, blacklists, or new APIs
---
## 🤝 Contributing
Contributions are welcome!
If youd like to enhance detection logic, optimize performance, or improve UI/UX — open a pull request or issue.
Let's build a safer digital world, together 💙
---
## 📜 License
This project is released under the **MIT License**.
```
MIT License
Copyright (c) 2025 zvspany
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the “Software”), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
```
---
## ❤️ Acknowledgements
Built by [@zvspany](https://zvspany.github.io/bio/)
Inspired by the fight against online scams, phishing, and misinformation.
Powered by the open-source community 🌍
---
🧩 *[AntiScam Pro](https://github.com/zvspany/antiscam-pro) — because cybersecurity should be transparent, intelligent, and free.*

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

235
api/endpoints.py Normal file
View File

@@ -0,0 +1,235 @@
# api/endpoints.py
import re
import os
import requests
import base64
from .utils import load_keywords, load_numbers, load_domains, load_shorteners, resolve_redirect
from .validators import Validators
from transformers import pipeline
import logging
logger = logging.getLogger(__name__)
# Import scanning function from a separate virustotal.py
from .virustotal import scan_url_with_virustotal
# VirusTotal API key is not needed here since the function in virustotal.py uses it
# Initialize AI model
try:
# suppress_warnings=True silences warnings during model loading
spam_classifier = pipeline("text-classification", model="mrm8488/bert-tiny-finetuned-sms-spam-detection")
logger.info("AI spam detection model loaded successfully.")
except Exception as e:
logger.warning(f"Failed to load AI model: {e}")
spam_classifier = None
# check_message - LOGIC FUNCTION
def check_message(text: str) -> dict:
"""Checks a text message for spam and suspicious words."""
result = {
"suspicious_words": [],
"ai_result": None,
"is_suspicious": False
}
keywords = load_keywords() # Assume load_keywords() works correctly
keywords = [kw.strip() for kw in keywords if kw.strip()] # Filter empty keywords
# Check for suspicious keywords (case-insensitive)
found_keywords = [kw for kw in keywords if kw.lower() in text.lower()]
result["suspicious_words"] = found_keywords
if spam_classifier:
try:
# Trim message to first 512 tokens for BERT
max_len = 512
if len(text.split()) > max_len:
text_for_ai = " ".join(text.split()[:max_len])
logger.warning(f"Message trimmed to {max_len} words for AI analysis.")
else:
text_for_ai = text
prediction = spam_classifier(text_for_ai)[0]
label = prediction["label"]
confidence = round(prediction["score"], 4)
result["ai_result"] = {
"label": "SPAM" if label == "LABEL_1" else "HAM", # LABEL_1 assumed to be SPAM
"confidence": confidence
}
# Suspicion if AI >= 0.6 OR keywords found
result["is_suspicious"] = (label == "LABEL_1" and confidence >= 0.6) or bool(result["suspicious_words"])
except Exception as ai_e:
logger.error(f"Error during AI message classification: {ai_e}")
result["ai_result"] = {"label": "ERROR", "confidence": 0}
# If AI fails, use only keyword check
result["is_suspicious"] = bool(result["suspicious_words"])
else:
result["is_suspicious"] = bool(result["suspicious_words"])
logger.info(f"check_message result: {result}")
return result
# check_phone - LOGIC FUNCTION
def check_phone(number: str) -> dict:
"""Checks a phone number for valid format and known scams."""
known_scams = load_numbers()
known_scams = {num.strip() for num in known_scams if num.strip()}
is_valid = Validators.is_valid_phone(number)
number_for_check = number.lstrip('+') # Remove '+' for database check
is_suspicious = number_for_check in known_scams
result = {
"is_valid": is_valid,
"is_suspicious": is_suspicious
}
logger.info(f"check_phone result for {number}: {result}")
return result
# check_link - LOGIC FUNCTION
def check_link(link: str) -> dict:
"""Checks a URL for suspicious patterns, known scam domains, shorteners, and scans with VirusTotal."""
logger.info(f"Checking link: {link}")
try:
link_resolved = resolve_redirect(link)
logger.info(f"Resolved link: {link_resolved}")
except Exception as resolve_e:
logger.error(f"Error resolving link {link}: {resolve_e}")
return {
"is_valid": False,
"is_suspicious": True,
"details": [{
"text": f"Error resolving redirect: {resolve_e}",
"data-pl": f"Błąd rozwiązywania przekierowania: {resolve_e}",
"data-en": f"Error resolving redirect: {resolve_e}"
}],
"source": "local"
}
link_clean = link_resolved.lower()
suspicious_reasons = []
is_valid = Validators.is_valid_url(link_resolved)
if not is_valid:
logger.warning(f"Resolved link is not a valid URL: {link_resolved}")
return {
"is_valid": False,
"is_suspicious": False,
"details": [{
"text": "Invalid resolved URL format",
"data-pl": "Niepoprawny format rozwiązanego URL",
"data-en": "Invalid resolved URL format"
}],
"source": "local"
}
# Local analysis - suspicious patterns in URL
suspicious_patterns = ["free", "gift", "login", "verify", "paypal", "paypa1", "bank", ".ru", ".cn"]
for pattern in suspicious_patterns:
if pattern in link_clean:
suspicious_reasons.append({
"text": f"Suspicious pattern found in URL: {pattern}",
"data-pl": f"Podejrzany wzorzec '{pattern}' znaleziony w adresie URL.",
"data-en": f"Suspicious pattern '{pattern}' found in the URL."
})
logger.info(f"Suspicious pattern '{pattern}' found in link: {link_resolved}")
# Local analysis - scam domains from database
scam_domains = load_domains()
scam_domains = [domain.strip() for domain in scam_domains if domain.strip()]
for domain in scam_domains:
if domain and domain in link_clean:
suspicious_reasons.append({
"text": f"Domain '{domain}' marked as suspicious in our database.",
"data-pl": f"Domena '{domain}' oznaczona jako podejrzana w naszej bazie danych.",
"data-en": f"Domain '{domain}' marked as suspicious in our database."
})
logger.info(f"Suspicious domain '{domain}' found in link: {link_resolved}")
# Local analysis - URL shorteners (check original link)
shorteners = load_shorteners()
shorteners = [short.strip() for short in shorteners if short.strip()]
is_shortened = False
for short in shorteners:
if short and short in link.lower():
is_shortened = True
suspicious_reasons.append({
"text": f"Link shortener detected in original link: {short}",
"data-pl": f"Wykryto usługę skracania URL w oryginalnym linku: {short}",
"data-en": f"Link shortener detected in original link: {short}"
})
logger.info(f"URL shortener '{short}' detected in original link: {link}")
local_is_suspicious = bool(suspicious_reasons)
# VirusTotal scan: only if local suspicious findings or short link
vt_result = {"detected": False, "positives": 0, "total": 0, "scan_date": "N/A", "error": "Not scanned locally"}
if local_is_suspicious or is_shortened:
logger.info(f"Local suspicious or short link -> checking VirusTotal for: {link_resolved}")
vt_result = scan_url_with_virustotal(link_resolved)
logger.info(f"VirusTotal result for {link_resolved}: {vt_result}")
vt_detected = vt_result.get("detected", False)
vt_positives = vt_result.get("positives", 0)
vt_total = vt_result.get("total", 0)
vt_scan_date = vt_result.get("scan_date", "N/A")
vt_error = vt_result.get("error")
combined_details = []
if local_is_suspicious:
combined_details.extend(suspicious_reasons)
if vt_result.get('error') != "Not scanned locally":
if vt_error and vt_error != "Not scanned locally":
vt_summary_text = f"VirusTotal: Error - {vt_error}"
vt_summary_pl = f"VirusTotal: Błąd - {vt_error}"
vt_summary_en = f"VirusTotal: Error - {vt_error}"
else:
vt_summary_text = f"VirusTotal: {vt_positives} / {vt_total} engines flagged the link."
vt_summary_pl = f"VirusTotal: {vt_positives} / {vt_total} silników oznaczyło link."
vt_summary_en = f"VirusTotal: {vt_positives} / {vt_total} engines flagged the link."
if vt_scan_date and vt_scan_date != "N/A":
vt_summary_text += f" Scan date: {vt_scan_date}."
vt_summary_pl += f" Scan date: {vt_scan_date}."
vt_summary_en += f" Scan date: {vt_scan_date}."
combined_details.append({
"text": vt_summary_text,
"data-pl": vt_summary_pl,
"data-en": vt_summary_en
})
else:
logger.info(f"VirusTotal scan skipped for link: {link_resolved} (local analysis clean and not shortened).")
final_is_suspicious = local_is_suspicious or vt_detected
if not combined_details:
combined_details.append({
"text": "No suspicious activity detected based on available checks.",
"data-pl": "Nie wykryto podejrzanej aktywności na podstawie dostępnych sprawdzeń.",
"data-en": "No suspicious activity detected based on available checks."
})
source = "none"
elif local_is_suspicious and vt_detected:
source = "combined"
elif vt_detected:
source = "virustotal"
elif local_is_suspicious:
source = "local"
else:
source = "unknown"
result = {
"is_valid": is_valid,
"is_suspicious": final_is_suspicious,
"details": combined_details,
"source": source
}
logger.info(f"Final result for check_link ({link}): {result}")
return result

53
api/utils.py Normal file
View File

@@ -0,0 +1,53 @@
# api/utils.py
import requests
def load_keywords():
"""Load scam-related keywords from file."""
try:
with open("data/scam_keywords.txt", "r", encoding="utf-8") as f:
# Ensure keywords are converted to lowercase
return [line.strip().lower() for line in f]
except FileNotFoundError:
print("File 'scam_keywords.txt' not found.")
return []
def load_numbers():
"""Load scam phone numbers from file."""
try:
with open("data/scam_numbers.txt", "r", encoding="utf-8") as f:
return [line.strip() for line in f]
except FileNotFoundError:
print("File 'scam_numbers.txt' not found.")
return []
def load_domains():
"""Load known scam domains from file."""
try:
with open("data/scam_domains.txt", "r", encoding="utf-8") as f:
return [line.strip().lower() for line in f]
except FileNotFoundError:
print("File 'scam_domains.txt' not found.")
return []
def load_shorteners():
"""Load known URL shorteners from file."""
try:
with open("data/url_shorteners.txt", "r", encoding="utf-8") as f:
return [line.strip().lower() for line in f]
except FileNotFoundError:
print("File 'url_shorteners.txt' not found.")
return []
def resolve_redirect(url: str) -> str:
"""
Resolves redirects for shortened URLs like bit.ly, tinyurl, etc.
Returns the final URL or the original if redirect fails.
"""
try:
response = requests.head(url, allow_redirects=True, timeout=5)
return response.url
except Exception as e:
print(f"[WARN] Failed to resolve redirect: {e}")
return url

102
api/validators.py Normal file
View File

@@ -0,0 +1,102 @@
import re
from typing import Optional
class Validators:
@staticmethod
def is_valid_phone(number: str) -> bool:
"""
Validate phone number format (E.164 with optional +)
Allowed formats:
+48123456789
48123456789
123456789
"""
return re.match(r"^\+?\d[\d\s-]{8,14}\d$", number) is not None
@staticmethod
def is_valid_email(email: str) -> bool:
"""
Validate email format
"""
return re.match(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$", email) is not None
@staticmethod
def is_valid_password(password: str, min_length: int = 8) -> bool:
"""
Validate password:
- Minimum length
- At least one digit
- At least one uppercase
- At least one lowercase
"""
if len(password) < min_length:
return False
if not re.search(r"\d", password):
return False
if not re.search(r"[A-Z]", password):
return False
if not re.search(r"[a-z]", password):
return False
return True
@staticmethod
def is_valid_username(username: str, min_length: int = 3, max_length: int = 20) -> bool:
"""
Validate username:
- Only alphanumeric and underscores
- Length between min and max
"""
return (re.match(r"^[a-zA-Z0-9_]+$", username) is not None and
min_length <= len(username) <= max_length)
@staticmethod
def is_valid_postal_code(code: str, country: str = 'PL') -> bool:
"""
Validate postal code format for different countries
Default: Polish format (00-000)
"""
if country == 'PL':
return re.match(r"^\d{2}-\d{3}$", code) is not None
# Add other country formats as needed
return False
@staticmethod
def is_valid_url(url: str) -> bool:
"""
Validate URL format
"""
return re.match(
r"^(https?://)?(www\.)?[a-z0-9-]+(\.[a-z]{2,}){1,}(/.*)?$",
url,
re.IGNORECASE
) is not None
@staticmethod
def is_length_valid(text: str, min_len: int = 0, max_len: Optional[int] = None) -> bool:
"""
Validate text length
"""
if max_len is None:
return len(text) >= min_len
return min_len <= len(text) <= max_len
@staticmethod
def is_numeric(text: str) -> bool:
"""
Check if text contains only digits
"""
return text.isdigit()
@staticmethod
def is_alpha(text: str) -> bool:
"""
Check if text contains only letters
"""
return text.isalpha()
@staticmethod
def is_alphanumeric(text: str) -> bool:
"""
Check if text contains only letters and digits
"""
return text.isalnum()

115
api/virustotal.py Normal file
View File

@@ -0,0 +1,115 @@
# api/virustotal.py
import requests
import os
import base64
import logging
from datetime import datetime # Only import datetime
logger = logging.getLogger(__name__)
# Retrieve API key from ENV or use default (should be changed!)
VIRUSTOTAL_API_KEY = os.getenv("VT_API_KEY")
# Warning if the key is not set
if not VIRUSTOTAL_API_KEY or VIRUSTOTAL_API_KEY == "YOUR_API_KEY":
logger.warning("VIRUSTOTAL_API_KEY is not set in environment variables or default value was not changed.")
# Optionally, you might want to raise an error or disable VT checks entirely if the key is missing.
# Removed imports of pytz and tzlocal and SERVER_LOCAL_TZ detection logic
def scan_url_with_virustotal(url: str) -> dict:
"""
Scans a URL using VirusTotal API v3.
Retrieves an existing scan report for the given URL.
(Does not initiate a new scan if the report does not exist)
Args:
url (str): URL to scan.
Returns:
dict: Dictionary containing VirusTotal scan results.
{
"detected": bool, # True if malicious or suspicious > 0
"positives": int, # Sum of malicious and suspicious counts
"total": int, # Sum of counts for harmless, malicious, suspicious, undetected, timeout, failure
"scan_date": str, # Formatted scan date string in UTC or "N/A"
"error": str # Error message if request fails
}
"""
if not VIRUSTOTAL_API_KEY or VIRUSTOTAL_API_KEY == "YOUR_API_KEY":
return {"detected": False, "positives": 0, "total": 0, "scan_date": "N/A", "error": "API key missing or default"}
api_url_base = "https://www.virustotal.com/api/v3/urls/"
headers = {"x-apikey": VIRUSTOTAL_API_KEY}
try:
# VirusTotal API v3 requires base64url encoded URL without padding
# https://developers.virustotal.com/v3.0/reference/#urls-id
encoded_url = base64.urlsafe_b64encode(url.encode()).decode().strip("=")
# URL to fetch analysis
analysis_url = f"{api_url_base}{encoded_url}"
logger.info(f"Querying VirusTotal for URL: {url}")
response = requests.get(analysis_url, headers=headers)
# Check response status
if response.status_code == 404:
logger.info(f"VirusTotal: URL not found in database: {url}")
return {"detected": False, "positives": 0, "total": 0, "scan_date": "N/A", "message": "URL not found in VT database"}
elif response.status_code == 401:
logger.error("VirusTotal API error: Invalid API key")
return {"detected": False, "positives": 0, "total": 0, "scan_date": "N/A", "error": "Invalid API key"}
elif response.status_code == 429:
logger.warning("VirusTotal API error: Rate limit exceeded")
return {"detected": False, "positives": 0, "total": 0, "scan_date": "N/A", "error": "Rate limit exceeded"}
elif response.status_code >= 400: # Handle other 4xx/5xx HTTP errors
error_message = f"HTTP error {response.status_code}"
try:
error_data = response.json()
error_message += f": {error_data.get('error', {}).get('message', 'Unknown VT error')}"
except:
pass # Ignore if JSON parsing fails
logger.error(f"VirusTotal HTTP error for {url}: {error_message}")
return {"detected": False, "positives": 0, "total": 0, "scan_date": "N/A", "error": error_message}
# If status 200 OK, process data
data = response.json()
attributes = data.get("data", {}).get("attributes", {})
stats = attributes.get("last_analysis_stats", {})
malicious = stats.get("malicious", 0)
suspicious = stats.get("suspicious", 0)
# Calculate total engines based on the stats provided
total_engines = stats.get("harmless", 0) + malicious + suspicious + stats.get("undetected", 0) + stats.get("timeout", 0) + stats.get("failure", 0)
scan_date_ts = attributes.get("last_analysis_date") # Timestamp (seconds since epoch UTC)
scan_date_str = "N/A"
if scan_date_ts:
try:
# Convert timestamp to naive UTC datetime object
utc_dt = datetime.utcfromtimestamp(scan_date_ts)
# Format date and time, append " UTC"
scan_date_str = utc_dt.strftime('%Y-%m-%d %H:%M:%S') + ' UTC'
except Exception as date_e:
logger.error(f"Error formatting VirusTotal scan date timestamp {scan_date_ts}: {date_e}")
scan_date_str = "Invalid Date Format"
return {
"detected": (malicious + suspicious) > 0,
"positives": malicious + suspicious,
"total": total_engines,
"scan_date": scan_date_str, # Return formatted date string in UTC
"error": None # No error
}
except requests.exceptions.RequestException as req_err:
logger.error(f"VirusTotal request failed for {url}: {req_err}")
return {"detected": False, "positives": 0, "total": 0, "scan_date": "N/A", "error": f"Request failed: {req_err}"}
except Exception as e:
logger.error(f"An unexpected error occurred during VirusTotal check for {url}: {e}")
return {"detected": False, "positives": 0, "total": 0, "scan_date": "N/A", "error": f"Unexpected error: {e}"}

189
app.py Normal file
View File

@@ -0,0 +1,189 @@
# app.py
import os
import logging
from flask import Flask, render_template, request, jsonify, session, redirect, url_for
# Import functions from api/endpoints.py
# check_link is assumed to handle VT internally and return combined result for 'link' key
from api.endpoints import check_message, check_phone, check_link
# Import Validators from api/validators.py - Needed for type detection
from api.validators import Validators
# Konfiguracja logowania
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
app = Flask(__name__)
# Setting secret_key is crucial for session security. Change this in production!
app.secret_key = os.getenv("FLASK_SECRET_KEY", "default_key_that_should_be_changed")
# Global statistics (since server start)
GLOBAL_STATS = {
"messages_checked": 0,
"phones_checked": 0,
"links_checked": 0
}
def increment_stat(key):
"""Increments session and global stats."""
# Session
session[key] = session.get(key, 0) + 1
# Global
GLOBAL_STATS[key] += 1
# ZASTĄP CAŁĄ FUNKCJĘ index PONIŻSZYM KODEM
@app.route("/", methods=["GET", "POST"])
def index():
"""
Handles the main page, processing form submissions (POST)
and displaying results after redirect (GET).
Implements Post/Redirect/Get (PRG) pattern.
Processes a single input field, detecting data type (Link, Phone, Message).
"""
user_ip = request.remote_addr
# Initialize session stats if they don't exist
if "messages_checked" not in session:
session["messages_checked"] = 0
session["phones_checked"] = 0
session["links_checked"] = 0
logger.info(f"Request to index ({request.method}) from IP: %s", user_ip)
# --- Handle POST Request (Process & Store, then Redirect) ---
if request.method == "POST":
# Clear previous results from session before processing new ones
session.pop('check_results', None)
# Get data from the single input field named "input_data"
input_data = request.form.get("input_data", "").strip() # Get and strip whitespace
current_check_results = {}
if input_data:
logger.info(f"Received input data: '{input_data}'")
# --- Type Detection Logic ---
# Implement a simple prioritization: Link -> Phone -> Message
# This order can be adjusted based on expected input types
# 1. Check if it's a Link (URL) first
# Use Validators.is_valid_url from api/validators.py
if Validators.is_valid_url(input_data):
logger.info(f"Input detected as URL: {input_data}")
# Call check_link. Based on your endpoints.py, it returns the combined result dictionary for 'link'
link_result = check_link(input_data)
# Store the result dictionary under the 'link' key for the HTML template to read
current_check_results["link"] = link_result
increment_stat("links_checked") # Increment link stat
logger.info(f"Processed link check. Result: {link_result.get('is_suspicious', 'N/A')}")
# 2. Else, check if it's a Phone Number
# Use Validators.is_valid_phone from api/validators.py
elif Validators.is_valid_phone(input_data):
logger.info(f"Input detected as Phone: {input_data}")
phone_result = check_phone(input_data)
# Store the result dictionary under the 'phone' key for the HTML template
current_check_results["phone"] = phone_result
increment_stat("phones_checked") # Increment phone stat
logger.info(f"Processed phone check. Result: {phone_result.get('is_suspicious', 'N/A')}")
# 3. Else, if it's neither a valid URL nor a valid phone number, assume it's a Message
else:
logger.info(f"Input treated as Message: {input_data}")
message_result = check_message(input_data)
# Store the result dictionary under the 'message' key for the HTML template
current_check_results["message"] = message_result
increment_stat("messages_checked") # Increment message stat
logger.info(f"Processed message check. Result: {message_result.get('is_suspicious', 'N/A')}")
else:
logger.info("Received empty input data.")
# If input is empty, current_check_results will be empty.
# The HTML template is designed to handle this gracefully (no results displayed).
# Store the results from this single check in the session to be retrieved by the subsequent GET request
session['check_results'] = current_check_results
# Redirect to the same URL with GET method to display results and prevent form re-submission on refresh
logger.info("Redirecting after POST.")
return redirect(url_for('index'))
# --- Handle GET Request (Retrieve & Display) ---
else: # request.method == "GET"
# Retrieve the results from session if they exist (after a POST redirect)
# Use session.pop to get the value and remove it in one step. Defaults to empty dict if no results in session.
result = session.pop('check_results', {})
logger.info(f"Handling GET request. Results retrieved from session: {bool(result)}")
# Prepare session stats to pass to the template
session_stats = {
"messages_checked": session.get("messages_checked", 0),
"phones_checked": session.get("phones_checked", 0),
"links_checked": session.get("links_checked", 0)
}
# Render the template with the retrieved results (or empty result dict for a normal initial GET)
# The 'result' dictionary will contain 'message', 'phone', or 'link' key based on input type detected in POST
return render_template("index.html",
result=result, # Pass the result dictionary (can be empty or contain one check result)
global_stats=GLOBAL_STATS,
session_stats=session_stats)
# --- API ENDPOINTS ---
# These endpoints are designed for specific input types and typically return JSON.
# They remain unchanged as they are separate from the main single-input form.
@app.route("/api/check_message", methods=["POST"])
def api_check_message():
"""API endpoint for message check."""
data = request.get_json()
message = data.get("message", "")
if not message:
logger.warning("API check_message: Missing 'message' parameter.")
return jsonify({"error": "Missing 'message' parameter."}), 400
message_result = check_message(message)
increment_stat("messages_checked") # Increment message stat for API
logger.info(f"API check_message: Result: {message_result.get('is_suspicious', 'N/A')}")
return jsonify(message_result)
@app.route("/api/check_phone", methods=["POST"])
def api_check_phone():
"""API endpoint for phone check."""
data = request.get_json()
phone = data.get("phone_number", "")
if not phone:
logger.warning("API check_phone: Missing 'phone_number' parameter.")
return jsonify({"error": "Missing 'phone_number' parameter."}), 400
phone_result = check_phone(phone)
increment_stat("phones_checked") # Increment phone stat for API
logger.info(f"API check_phone: Result: {phone_result.get('is_suspicious', 'N/A')}")
return jsonify(phone_result)
@app.route("/api/check_link", methods=["POST"])
def api_check_link():
"""API endpoint for link check."""
data = request.get_json()
url = data.get("url", "")
if not url:
logger.warning("API check_link: Missing 'url' parameter.")
return jsonify({"error": "Missing 'url' parameter."}), 400
# Note: check_link in your endpoints.py handles VT internally and returns the combined dict
link_result = check_link(url)
increment_stat("links_checked") # Increment link stat for API
logger.info(f"API check_link: Result: {link_result.get('is_suspicious', 'N/A')}, Source: {link_result.get('source', 'N/A')}")
# Return the combined result dictionary from check_link
return jsonify(link_result)
# --- Application Entry Point ---
if __name__ == "__main__":
# Port from environment variable or default 5000
port = int(os.environ.get("PORT", 5000))
# host='0.0.0.0' allows external access (e.g., in Docker)
# debug=True is useful during development
app.run(host="0.0.0.0", port=port, debug=True)

183590
data/scam_domains.txt Normal file

File diff suppressed because it is too large Load Diff

347
data/scam_keywords.txt Normal file
View File

@@ -0,0 +1,347 @@
darmowe pieniądze
zyskaj natychmiast
pilna oferta
oferta tylko teraz
ograniczona oferta
ostateczna oferta
zyskaj teraz
potwierdź dane
weryfikacja konta
nagroda czeka
potrzebujemy twoich danych
potwierdzenie tożsamości
potwierdź numer telefonu
sprawdź konto
bezzwłoczna aktualizacja
zaktualizuj dane
wygasłe konto
decyzja musi być podjęta teraz
twoje konto zostało zablokowane
pilne konto
zaloguj się teraz
konkurs wygrany
nagroda czeka na ciebie
darmowy prezent
zyskaj miliony
potwierdź adres e-mail
wejdź na naszą stronę
przyjdź po swoją nagrodę
bezwysiłkowe zarobki
proste rozwiązanie
płatności bez ryzyka
inwestycje gwarantowane
weź udział teraz
nagroda dla ciebie
bezpłatna rejestracja
specjalna oferta
darmowe testy
otrzymaj kod rabatowy
zarejestruj się teraz
szybkie pieniądze
wygraj teraz
zapłać teraz
oferta tylko dla ciebie
kliknij tutaj
wejdź teraz
ograniczony dostęp
bądź pierwszy
podaj swój numer
potwierdź swoje dane
zabezpieczenia konta
natychmiastowy dostęp
musisz kliknąć
dzięki za zaufanie
otwórz teraz
kluczowe informacje
twoje konto zostało zawieszone
konto zablokowane
zaloguj się, aby odblokować
pożyczka bez weryfikacji
pożyczka online
zainwestuj teraz
szybka pożyczka
weź kredyt teraz
kryptowaluty
bitcoin inwestycje
wypłać teraz
twój bonus
zyskowna inwestycja
prosta metoda
zarabiaj online
zyskaj dodatkowy dochód
nie czekaj
twoje dane są wymagane
weryfikacja danych
odblokuj dostęp
oferta wyłącznie dla ciebie
bezzwłoczna weryfikacja
automatyczne zyski
zysk w 24 godziny
wejdź na naszą stronę
płatność online
kliknij, aby kontynuować
sprawdzony sposób na zarobek
płatności gwarantowane
zyskaj dostęp teraz
kliknij, aby wygrać
przekaż pieniądze teraz
trendy inwestycyjne
darmowa próbka
wygraj nagrodę
kliknij, aby rozpocząć
darmowa aplikacja
zarejestruj się za darmo
wykorzystaj teraz
oferta bez zobowiązań
pełna gwarancja
natychmiastowy dostęp do konta
szansa na wygraną
wyślij teraz
czekamy na Ciebie
potwierdź teraz
tylko dla wybranych
bądź jednym z wygranych
promocja ograniczona czasowo
wejdź i sprawdź
usunięcie konta
wygodna pożyczka
sprawdź saldo
oferta bez ryzyka
wypełnij formularz
twoje dane są weryfikowane
zarejestruj się na platformie
sprawdź wyniki
oferta tylko dzisiaj
odblokuj teraz
kryptowaluty inwestycja
pożyczka online bez formalności
bezpieczna inwestycja
zyskaj bezpieczeństwo
dodatkowe zabezpieczenie
tylko dla nowych użytkowników
dodatkowe środki na koncie
szybka pożyczka online
nagroda czeka na ciebie
bezpieczna transakcja
potwierdź telefonicznie
darmowa karta kredytowa
oferta niepowtarzalna
łatwy sposób na sukces
proste rozwiązanie
zainwestuj teraz
oferta wyłącznie przez 24 godziny
kryptowaluty pożyczka
kup teraz
natychmiastowa transakcja
oferta czasowa
błyskawiczne pożyczki
wejdź teraz
zainwestuj bez ryzyka
inwestycje bez formalności
oferta specjalna
czekają na ciebie
inwestycja w przyszłość
spróbuj teraz
zyskaj dla siebie
oferta tylko przez 24 godziny
zainwestuj bez opóźnień
pożyczka bez weryfikacji
sprawdź nasze oferty
kliknij tutaj, aby wygrać
kryptowaluty bez ryzyka
zapłać teraz
pożyczka do 10000 zł
wygraj teraz
zapłać tylko 1 zł
tylko teraz
oferta dnia
kliknij, aby zdobyć bonus
darmowe pieniądze online
wygraj pożyczkę
wygrana czeka
zyskaj dla siebie
zarejestruj się teraz
aktywuj konto
darmowy dostęp
oferta czasowa
bez ryzyka
otwórz konto teraz
pożyczka na dowolny cel
sprawdzona metoda zarobku
otrzymaj teraz
potwierdź e-mail
weryfikacja numeru telefonu
płatności w 24h
wygraj w konkursie
darmowy kredyt
sprawdź pożyczki
wygodne pożyczki
otrzymaj pełny dostęp
chciałbyś wybrać gotówkę?
płatności online teraz
opóźniona weryfikacja
nie przegap okazji
przyjdź po swoją nagrodę
zainwestuj w kryptowaluty
inwestuj teraz
ograniczone inwestycje
profesjonalna pożyczka
oferta bez opłat
zyskaj ogromną nagrodę
przyjdź i wygraj
pożyczka z uproszczoną weryfikacją
free money
get rich now
urgent offer
limited time offer
exclusive offer
final offer
act now
confirm your details
account verification
confirm phone number
check your account
account expired
urgent account
login now
won a contest
prize waiting for you
free gift
get millions
confirm your email
visit our site
claim your reward
no effort earnings
guaranteed investment
take part now
reward for you
free registration
special offer
free trial
receive discount code
register now
quick cash
win now
pay now
offer just for you
click here
join now
limited access
be the first
provide your number
confirm your details
account security
instant access
you must click
thanks for your trust
open now
key information
your account has been suspended
account blocked
login to unlock
loan without verification
online loan
invest now
quick loan
get credit now
cryptocurrency
bitcoin investment
withdraw now
your bonus
profitable investment
simple method
earn online
additional income
dont wait
your details are needed
verify your details
unlock access
exclusive offer for you
immediate verification
automatic profits
profits in 24 hours
visit our site
online payment
click to continue
verified earnings
secure payment
earn money easily
instant reward
quick withdrawal
easy money
exclusive content
limited time access
urgent message
unmissable offer
make money now
urgent response required
special deal
become a winner now
limited time deal
secure transaction
immediate results
unlimited bonus
enter here now
invest without risk
simple way to earn
online bonus
easy profits
get started today
guaranteed money
special promotion
withdraw your profits
simple solution
limited time access
sign up now
act fast
investment opportunity
easy way to invest
instant payout
top opportunity
dont miss this chance
confirm now
no risk investment
get paid now
secure online payments
win big
new offers available
get free bonus
fast payouts
limited time only
secure your spot
instant confirmation
quick profits
fast money
loan with no verification
get rewards
get started now
confirm email
confirm phone number
take advantage of this offer
offer only available now
easy loan
verify now
check your winnings
free loans
secure loans
fast loans
access now
no payment required
easy registration
online payment system
earn rewards now
your payment is waiting
invest in crypto
safe investment
dont miss the chance
instant payout now
get access immediately
sign up today
free cryptocurrency
act quickly
unmissable opportunity
click to win
guaranteed rewards
free rewards
take action now

67
data/scam_numbers.txt Normal file
View File

@@ -0,0 +1,67 @@
+48795800000
+48600123456
+48777233444
+447700900123
+447800000000
+447400000000
+447500000000
+447600000000
+447900000000
+442080000000
+37200000000
+420000000000
+37000000000
+48000000000
+375000000000
+810000000000
+870000000000
+880000000000
48500123456
48501123456
48502123456
48503123456
48504123456
48505123456
48506123456
48507123456
48508123456
48509123456
70700
70701
70702
70800
70801
70802
70900
70901
70902
80000
80001
80002
80100
80101
80102
80200
80201
80202
80300
80301
80302
80400
80401
80402
80500
80501
80502
80600
80601
80602
80700
80701
80702
80800
80801
80802
80900
80901
80902

7
data/url_shorteners.txt Normal file
View File

@@ -0,0 +1,7 @@
bit.ly
tinyurl.com
t.co
goo.gl
rb.gy
is.gd
tiny.pl

9
requirements.txt Normal file
View File

@@ -0,0 +1,9 @@
Flask
gunicorn
requests
transformers
torch
python-dotenv
numpy
pytz
tzlocal

69
static/css/style.css Normal file
View File

@@ -0,0 +1,69 @@
body {
transition: background-color 0.4s, color 0.4s;
}
.transition-theme {
background-color: #f8f9fa;
color: #212529;
}
.transition-theme.dark-mode {
background-color: #121212;
color: #e9ecef;
}
.dark-mode .bg-body-tertiary {
background-color: #1e1e1e !important;
}
.dark-mode .form-control,
.dark-mode .form-label {
background-color: #2c2c2c;
color: #e9ecef;
border-color: #444;
}
.dark-mode .form-control::placeholder {
color: #bbb;
}
.dark-mode .alert-success {
background-color: #224422;
color: #cfc;
border-color: #3d3;
}
.dark-mode .alert-danger {
background-color: #441111;
color: #fbb;
border-color: #e33;
}
/* Style dla motywu jasnego (domyślne) */
.transition-theme {
transition: all 0.3s ease;
background-color: #ffffff;
color: #212529;
border-color: #dee2e6;
}
/* Style dla motywu ciemnego */
body.dark-mode .transition-theme {
background-color: #2d2d2d;
color: #f8f9fa;
border-color: #444;
}
body.dark-mode .list-group-item.transition-theme {
background-color: #2d2d2d;
color: #f8f9fa;
border-color: #444;
}
body.dark-mode .text-muted.transition-theme {
color: #adb5bd !important;
}
body.dark-mode .card.transition-theme {
background-color: #2d2d2d;
border-color: #444;
}

312
templates/index.html Normal file
View File

@@ -0,0 +1,312 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>AntiScam Pro</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" rel="stylesheet">
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body class="transition-theme">
<div class="container py-5">
<!-- Standard Bootstrap container (responsive) -->
<!-- Header with title and buttons - using responsive flexbox -->
<!-- d-flex justify-content-between align-items-center is a default horizontal flexbox -->
<!-- flex-column-sm-row: stack in column on < sm, row on >= sm -->
<div class="d-flex justify-content-between align-items-center mb-4 flex-column flex-sm-row">
<!-- Title - bottom margin on small screens (mb-3), removed on >= sm (mb-sm-0) -->
<h1 class="fw-bold mb-3 mb-sm-0">AntiScam Pro</h1>
<!-- Button container - also stacked on small screens -->
<div class="d-flex flex-column flex-sm-row">
<!-- Theme button - bottom margin on small, right margin on >= sm -->
<button id="toggleTheme" class="btn btn-outline-secondary mb-2 me-sm-2">
<i class="fas fa-moon"></i> <span id="theme-btn-text" data-pl="Zmień motyw" data-en="Toggle theme">Toggle theme</span>
</button>
<!-- Language toggle button -->
<button id="toggleLanguage" class="btn btn-outline-secondary">
<i class="fas fa-language"></i> <span id="lang-btn-text">PL</span>
</button>
</div>
</div>
<!-- Form section -->
<form method="POST" class="bg-body-tertiary p-4 rounded shadow-sm">
<input type="hidden" name="lang" id="langInput" value="en">
<div class="mb-3">
<!-- Single textarea instead of 3 inputs -->
<label for="single_input" class="form-label" data-pl="Wprowadź dane do analizy:" data-en="Enter data for analysis:">Enter data for analysis:</label>
<textarea name="input_data" id="single_input" rows="3" class="form-control"
data-pl-placeholder="Wprowadź wiadomość, numer telefonu lub link..."
data-en-placeholder="Enter message, phone number, or link..."
placeholder="Enter message, phone number, or link..."></textarea>
</div>
<!-- Submit button -->
<button type="submit" class="btn btn-primary" data-pl="Sprawdź" data-en="Check">Check</button>
</form>
<!-- Results section -->
<div class="mt-4">
<!-- Message result -->
{% if result.message %}
<div class="alert {{ 'alert-danger' if result.message.is_suspicious else 'alert-success' }}">
<strong data-pl="Wiadomość:" data-en="Message:">Message:</strong>
{% if result.message.is_suspicious %}
{% if result.message.suspicious_words %}
<span data-pl="Podejrzana. Znalezione wyrazy:" data-en="Suspicious. Found words:">Suspicious. Found words:</span>
<em>{{ result.message.suspicious_words | join(', ') }}</em>
{% endif %}
{% if result.message.ai_result %}
<p data-pl="Wykryte przez AI:" data-en="Detected by AI:">Detected by AI:</p>
<p>{{ result.message.ai_result.label }} ({{ (result.message.ai_result.confidence * 100) | round(1) }}%)</p>
{% endif %}
{% else %}
<span data-pl="Brak podejrzanych treści." data-en="No suspicious content.">No suspicious content.</span>
{% endif %}
</div>
{% endif %}
<!-- Phone number result -->
{% if result.phone %}
<div class="alert {{ 'alert-danger' if result.phone.is_suspicious else 'alert-success' }}">
<strong data-pl="Numer telefonu:" data-en="Phone number:">Phone number:</strong>
{% if result.phone.is_suspicious %}
<span data-pl="Podejrzany numer." data-en="Suspicious number.">Suspicious number.</span>
<p data-pl="Powód podejrzenia: Numer w bazie danych podejrzanych numerów." data-en="Reason: Number found in suspicious database.">
Reason: Number found in suspicious database.
</p>
{% else %}
<span data-pl="Wygląda na bezpieczny." data-en="Looks safe.">Looks safe.</span>
{% endif %}
</div>
{% endif %}
<!-- Link result section -->
{% if result.link is defined and (result.link.is_suspicious or result.link.details) %}
{% set overall_link_suspicious_display = result.link.is_suspicious %}
<div class="alert {{ 'alert-danger' if overall_link_suspicious_display else 'alert-success' }}">
<strong data-pl="Link:" data-en="Link:">Link:</strong>
{% if overall_link_suspicious_display %}
<span style="color: red;" data-pl="Podejrzany." data-en="Suspicious.">Suspicious.</span>
{% else %}
<span style="color: green;" data-pl="Brak podejrzanej aktywności." data-en="No suspicious activity detected.">No suspicious activity detected.</span>
{% endif %}
{% if result.link.details %}
<p data-pl="Szczegóły analizy:" data-en="Analysis Details:">Analysis Details:</p>
<ul>
{% for detail in result.link.details %}
<li>
<span data-pl="{{ detail.get('data-pl', detail.get('text', '')) }}" data-en="{{ detail.get('data-en', detail.get('text', '')) }}">
{{ detail.get('text', 'Unknown analysis detail') }}
</span>
</li>
{% endfor %}
</ul>
{% elif not overall_link_suspicious_display %}
<p data-pl="Brak dodatkowych szczegółów analizy." data-en="No additional analysis details.">No additional analysis details.</p>
{% endif %}
{% if result.link is defined and result.link.is_valid is defined %}
<p data-pl="Format URL poprawny: {{ 'Tak' if result.link.is_valid else 'Nie' }}" data-en="URL format valid: {{ 'Yes' if result.link.is_valid else 'No' }}">URL format valid: {{ 'Yes' if result.link.is_valid else 'No' }}</p>
{% endif %}
</div>
{% elif result.link is not defined %}
<div class="alert alert-info">
<span data-pl="Podaj dane do analizy." data-en="Provide data for analysis.">Provide data for analysis.</span>
</div>
{% endif %}
</div>
<!-- Info and Statistics section -->
<div class="container mt-4">
<div class="row g-4">
<div class="col-md-6">
<div class="card h-100 transition-theme">
<div class="card-body">
<h2 class="card-title" data-pl="O AntiScam Pro" data-en="About AntiScam Pro">About AntiScam Pro</h2>
<p class="card-text" data-pl="AntiScam Pro to zaawansowana aplikacja do wykrywania oszustw..." data-en="AntiScam Pro is an advanced tool for scam detection...">
AntiScam Pro is an advanced application for detecting scams, spam, and suspicious links. It uses AI and databases to ensure safety.
</p>
<h3 class="card-title" data-pl="Funkcje:" data-en="Features:">Features:</h3>
<ul class="list-group list-group-flush">
<li class="list-group-item transition-theme" data-pl="Wykrywanie podejrzanych wiadomości tekstowych" data-en="Detects suspicious text messages">Detects suspicious text messages</li>
<li class="list-group-item transition-theme" data-pl="Weryfikacja numerów telefonów i linków" data-en="Phone number and link verification">Phone number and link verification</li>
<li class="list-group-item transition-theme">
<details>
<summary data-pl="Integracja z VirusTotal" data-en="Integration with VirusTotal">Integration with VirusTotal</summary>
<p class="text-muted mt-2 transition-theme" data-pl="Analiza linków obejmuje skanowanie VirusTotal..." data-en="Link analysis includes VirusTotal scanning if API key is configured and link requires verification.">
Link analysis includes VirusTotal scanning if API key is configured and link requires verification.
</p>
</details>
</li>
<li class="list-group-item transition-theme">
<details>
<summary data-pl="Obszerne bazy danych" data-en="Extensive databases">Extensive databases</summary>
<p class="text-muted mt-2 transition-theme" data-pl="Nasza baza zawiera..." data-en="Our database contains over 186,000 suspicious domains.">
Our database contains over 186,000 suspicious domains.
</p>
</details>
</li>
</ul>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card h-100 transition-theme">
<div class="card-body">
<h3 class="card-title" data-pl="📊 Statystyki globalne" data-en="📊 Global statistics">📊 Global statistics</h3>
<p class="text-muted transition-theme" data-pl="(od uruchomienia serwera)" data-en="(since server start)">(since server start)</p>
<ul class="list-group list-group-flush">
<li class="list-group-item transition-theme">
<span data-pl="Wiadomości sprawdzone:" data-en="Messages checked:">Messages checked:</span>
<span class="badge bg-primary">{{ global_stats.messages_checked }}</span>
</li>
<li class="list-group-item transition-theme">
<span data-pl="Numery sprawdzone:" data-en="Phones checked:">Phones checked:</span>
<span class="badge bg-primary">{{ global_stats.phones_checked }}</span>
</li>
<li class="list-group-item transition-theme">
<span data-pl="Linki sprawdzone:" data-en="Links checked:">Links checked:</span>
<span class="badge bg-primary">{{ global_stats.links_checked }}</span>
</li>
</ul>
<h3 class="card-title mt-4" data-pl="👤 Twoje statystyki" data-en="👤 Your statistics">👤 Your statistics</h3>
<p class="text-muted transition-theme" data-pl="(ta sesja)" data-en="(this session)">(this session)</p>
<ul class="list-group list-group-flush">
<li class="list-group-item transition-theme">
<span data-pl="Wiadomości sprawdzone:" data-en="Messages checked:">Messages checked:</span>
<span class="badge bg-info">{{ session_stats.messages_checked }}</span>
</li>
<li class="list-group-item transition-theme">
<span data-pl="Numery sprawdzone:" data-en="Phones checked:">Phones checked:</span>
<span class="badge bg-info">{{ session_stats.phones_checked }}</span>
</li>
<li class="list-group-item transition-theme">
<span data-pl="Linki sprawdzone:" data-en="Links checked:">Links checked:</span>
<span class="badge bg-info">{{ session_stats.links_checked }}</span>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- JS Scripts -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
const btnTheme = document.getElementById("toggleTheme");
const btnLang = document.getElementById("toggleLanguage");
const themeText = document.getElementById("theme-btn-text");
const langText = document.getElementById("lang-btn-text");
const langInput = document.getElementById("langInput");
const singleInput = document.getElementById("single_input");
// User system preference for dark mode
const prefersDarkMode = window.matchMedia('(prefers-color-scheme: dark)');
// Get preferences from localStorage or use system preference
let darkMode = localStorage.getItem("darkMode");
if (darkMode === null) {
darkMode = prefersDarkMode.matches;
} else {
darkMode = (darkMode === "true");
}
// Default language = English
let lang = localStorage.getItem("lang") || "en";
// Update theme button text and icon
function updateThemeButtonText(isDarkMode, currentLang) {
const textPl = isDarkMode ? "Jasny motyw" : "Ciemny motyw";
const textEn = isDarkMode ? "Light theme" : "Dark theme";
if (themeText) {
themeText.dataset.pl = textPl;
themeText.dataset.en = textEn;
themeText.textContent = currentLang === "pl" ? textPl : textEn;
}
const themeIcon = btnTheme ? btnTheme.querySelector('i') : null;
if (themeIcon) {
themeIcon.classList.toggle('fa-moon', isDarkMode);
themeIcon.classList.toggle('fa-sun', !isDarkMode);
}
}
// Apply theme
function applyTheme(isDarkMode) {
if (document.body) {
document.body.classList.toggle("dark-mode", isDarkMode);
updateThemeButtonText(isDarkMode, lang);
localStorage.setItem("darkMode", isDarkMode);
}
}
// Apply language + placeholder updates
function applyLanguage(language) {
document.querySelectorAll("[data-pl]").forEach(el => {
if (el.dataset.pl && el.dataset.en) {
el.textContent = el.dataset[language];
}
});
if (singleInput) {
singleInput.placeholder = singleInput.dataset[language + '-placeholder'] || singleInput.placeholder;
}
if (langText) {
langText.textContent = language === "pl" ? "EN" : "PL";
}
if (langInput) {
langInput.value = language;
}
localStorage.setItem("lang", language);
updateThemeButtonText(darkMode, language);
}
// Initial setup
applyTheme(darkMode);
applyLanguage(lang);
// Button listeners
if (btnTheme) {
btnTheme.addEventListener("click", () => {
darkMode = !darkMode;
applyTheme(darkMode);
});
}
if (btnLang) {
btnLang.addEventListener("click", () => {
lang = lang === "pl" ? "en" : "pl";
applyLanguage(lang);
});
}
// System preference listener
if (prefersDarkMode) {
prefersDarkMode.addListener(function(e) {
if (localStorage.getItem("darkMode") === null) {
applyTheme(e.matches);
}
});
}
console.log("AntiScam Pro custom JavaScript script finished execution.");
</script>
</body>
</html>