746 lines
27 KiB
Python
746 lines
27 KiB
Python
#!/usr/bin/env python3
|
||
|
||
import argparse
|
||
import mysql.connector
|
||
import dns.resolver
|
||
from datetime import datetime, timedelta
|
||
import csv
|
||
import socket
|
||
from dotenv import load_dotenv
|
||
import os
|
||
import sys
|
||
from tqdm import tqdm
|
||
import signal
|
||
import logging
|
||
import redis
|
||
from tabulate import tabulate
|
||
import xlsxwriter
|
||
from collections import defaultdict
|
||
import subprocess
|
||
import smtplib
|
||
from email.mime.multipart import MIMEMultipart
|
||
from email.mime.text import MIMEText
|
||
import time
|
||
import requests
|
||
import string
|
||
import textwrap
|
||
|
||
|
||
os.makedirs("logs", exist_ok=True)
|
||
os.makedirs("exports", exist_ok=True)
|
||
|
||
# Redis - baza 5
|
||
redis_client = redis.Redis(host='localhost', port=6379, db=5, decode_responses=True)
|
||
|
||
script_path = os.path.abspath(__file__)
|
||
|
||
# Tymczasowe domeny
|
||
DISPOSABLE_DOMAINS_URL = "https://raw.githubusercontent.com/disposable-email-domains/disposable-email-domains/refs/heads/main/disposable_email_blocklist.conf"
|
||
DISPOSABLE_DOMAINS_CACHE_KEY = "disposable_domains:list"
|
||
DISPOSABLE_DOMAINS_TTL = 86400 # 24h
|
||
|
||
epilog = textwrap.dedent(f"""
|
||
Przykłady użycia:
|
||
|
||
# 1. Podgląd nieaktywnych użytkowników bez punktów
|
||
{script_path} --days-inactive 730 --dry-run
|
||
|
||
# 2. Usuń (dezaktywuj) użytkowników z błędnymi e-mailami i nieaktywnych ponad 2 lata
|
||
{script_path} --days-inactive 730 --delete
|
||
|
||
# 3. Uwzględnij starych użytkowników (sprzed 2012), którzy logowali się w ciągu ostatnich 3 lat
|
||
{script_path} --days-inactive 730 --veteran-year 2012 --recent-login-days 1095
|
||
|
||
# 4. Walidacja poprawności adresów e-mail (bez usuwania)
|
||
{script_path} --validate
|
||
|
||
# 5. Czyszczenie cache rekordów MX w Redisie
|
||
{script_path} --flush-cache
|
||
|
||
# 6. Eksportuj dane użytkowników do pliku Excel
|
||
{script_path} --days-inactive 730 --dry-run --export-excel
|
||
|
||
# 7. Wygeneruj raport liczby użytkowników wg domen e-mail
|
||
{script_path} --days-inactive 730 --report-domains
|
||
|
||
# 8. Wyświetl tabelę z użytkownikami kwalifikującymi się do usunięcia
|
||
{script_path} --days-inactive 730 --show-table
|
||
|
||
# 9. Wyślij e-maile do użytkowników nieaktywnych od 1 do 5 lat
|
||
{script_path} --send-mails --inactive-since 365-1825
|
||
|
||
# 10. Wyślij testowego maila na podany adres
|
||
{script_path} --send-test test@example.com
|
||
|
||
# 11. Usuń (dezaktywuj) tylko użytkowników z nieprawidłowym lub tymczasowym adresem e-mail
|
||
{script_path} --only-invalid-emails --delete
|
||
|
||
# 12. Usuń (dezaktywuj) nieaktywnych użytkowników, z ustawioną ścieżką do Drupala
|
||
{script_path} --days-inactive 730 --delete --drupal-path /var/www/drupal
|
||
|
||
# 13. Ustaw inną liczbę maili i przerwę między paczkami (np. 50 maili co 30s)
|
||
{script_path} --send-mails --inactive-since 730-2000 --mails-per-pack 50 --time-per-pack 30
|
||
"""
|
||
)
|
||
|
||
# Logi
|
||
logging.basicConfig(
|
||
filename='logs/user_cleanup.log',
|
||
level=logging.INFO,
|
||
format='%(asctime)s [%(levelname)s] %(message)s',
|
||
datefmt='%Y-%m-%d %H:%M:%S'
|
||
)
|
||
|
||
def get_users(db_config):
|
||
connection = mysql.connector.connect(**db_config)
|
||
cursor = connection.cursor(dictionary=True)
|
||
query = """
|
||
SELECT u.uid, u.name, u.mail, u.access, u.created, p.points, COUNT(n.nid) AS post_count
|
||
FROM users u
|
||
LEFT JOIN node n ON u.uid = n.uid
|
||
LEFT JOIN userpoints p ON u.uid = p.uid
|
||
WHERE u.uid > 0
|
||
GROUP BY u.uid
|
||
"""
|
||
cursor.execute(query)
|
||
users = cursor.fetchall()
|
||
cursor.close()
|
||
connection.close()
|
||
return users
|
||
|
||
def search_user(db_config, usernames):
|
||
"""Wyszukuje wielu użytkowników po nazwach i wyświetla ich statystyki w tabeli."""
|
||
usernames_list = [name.strip() for name in usernames.split(',')]
|
||
|
||
connection = mysql.connector.connect(**db_config)
|
||
cursor = connection.cursor(dictionary=True)
|
||
|
||
query = """
|
||
SELECT
|
||
u.uid,
|
||
u.name,
|
||
u.mail,
|
||
u.access,
|
||
u.created,
|
||
u.login,
|
||
u.status,
|
||
p.points,
|
||
COUNT(n.nid) AS post_count
|
||
FROM users u
|
||
LEFT JOIN node n ON u.uid = n.uid
|
||
LEFT JOIN userpoints p ON u.uid = p.uid
|
||
WHERE u.name IN (%s)
|
||
GROUP BY u.uid
|
||
"""
|
||
|
||
# Przygotowanie parametrów dla IN w zapytaniu SQL
|
||
params = tuple(usernames_list)
|
||
query = query % (','.join(['%s'] * len(usernames_list)))
|
||
|
||
cursor.execute(query, params)
|
||
users = cursor.fetchall()
|
||
cursor.close()
|
||
connection.close()
|
||
|
||
if not users:
|
||
print(f"❌ Żaden z użytkowników ({usernames}) nie został znaleziony.")
|
||
return None
|
||
|
||
# Przygotowanie danych do wyświetlenia
|
||
headers = [
|
||
"UID", "Nazwa użytkownika", "E-mail", "Status konta",
|
||
"Data rejestracji", "Ostatnie logowanie", "Ostatnia aktywność",
|
||
"Dni nieaktywności", "Punkty", "Liczba postów",
|
||
"E-mail poprawny", "Tymczasowy e-mail", "Podejrzana nazwa"
|
||
]
|
||
|
||
# Transpozycja danych - każdy użytkownik jako kolumna
|
||
table_data = []
|
||
for header in headers:
|
||
row = [header]
|
||
for user in users:
|
||
# Dodatkowe informacje o e-mailu
|
||
temp_domains_cache = load_temp_domains()
|
||
email_valid = not is_fake_email(user['mail']) and not is_temp_email(user['mail'], temp_domains_cache)
|
||
bad_name = is_bad_name(user['name'])
|
||
|
||
# Oblicz dni nieaktywności
|
||
now_ts = int(datetime.now().timestamp())
|
||
last_access = user['access'] or 0
|
||
days_inactive = (now_ts - last_access) / 86400 if last_access else float('inf')
|
||
|
||
# Mapowanie danych
|
||
user_data = {
|
||
"UID": user['uid'],
|
||
"Nazwa użytkownika": user['name'],
|
||
"E-mail": user['mail'],
|
||
"Status konta": 'Aktywny' if user['status'] == 1 else 'Zablokowany',
|
||
"Data rejestracji": datetime.fromtimestamp(user['created']).strftime('%Y-%m-%d %H:%M:%S') if user.get('created') else 'Nieznana',
|
||
"Ostatnie logowanie": datetime.fromtimestamp(user['access']).strftime('%Y-%m-%d %H:%M:%S') if user.get('access') else 'Nigdy',
|
||
"Ostatnia aktywność": datetime.fromtimestamp(user['login']).strftime('%Y-%m-%d %H:%M:%S') if user.get('login') else 'Nigdy',
|
||
"Dni nieaktywności": f"{days_inactive:.1f} dni",
|
||
"Punkty": user.get('points', 0),
|
||
"Liczba postów": user.get('post_count', 0),
|
||
"E-mail poprawny": 'Tak' if email_valid else 'Nie',
|
||
"Tymczasowy e-mail": 'Tak' if is_temp_email(user['mail'], temp_domains_cache) else 'Nie',
|
||
"Podejrzana nazwa": 'Tak' if bad_name else 'Nie'
|
||
}
|
||
|
||
row.append(user_data.get(header, ''))
|
||
table_data.append(row)
|
||
|
||
# Wyświetl wyniki w tabeli
|
||
print("\n🔍 Porównanie użytkowników:")
|
||
print(tabulate(table_data, headers=[""] + [user['name'] for user in users], tablefmt="fancy_grid"))
|
||
|
||
return users
|
||
|
||
def is_fake_email(email):
|
||
try:
|
||
domain = email.split('@')[1]
|
||
cache_key = f"mx:{domain}"
|
||
cached = redis_client.get(cache_key)
|
||
if cached is not None:
|
||
return cached == "true"
|
||
answers = dns.resolver.resolve(domain, 'MX', lifetime=5.0)
|
||
result = "false" if answers else "true"
|
||
except Exception:
|
||
result = "true"
|
||
redis_client.set(cache_key, result, ex=259200)
|
||
return result == "true"
|
||
|
||
def load_temp_domains():
|
||
cached = redis_client.get(DISPOSABLE_DOMAINS_CACHE_KEY)
|
||
if cached:
|
||
return set(cached.split(","))
|
||
|
||
try:
|
||
resp = requests.get(DISPOSABLE_DOMAINS_URL, timeout=10)
|
||
if resp.status_code == 200:
|
||
domains = {line.strip().lower() for line in resp.text.splitlines() if line.strip() and not line.startswith("#")}
|
||
redis_client.set(DISPOSABLE_DOMAINS_CACHE_KEY, ",".join(domains), ex=DISPOSABLE_DOMAINS_TTL)
|
||
return domains
|
||
else:
|
||
print(f"⚠️ Nie udało się pobrać listy disposable domains (kod {resp.status_code})")
|
||
except Exception as e:
|
||
print(f"⚠️ Błąd pobierania listy disposable domains: {e}")
|
||
|
||
return set()
|
||
|
||
def is_temp_email(email, temp_domains_cache=None):
|
||
try:
|
||
domain = email.split('@')[1].lower()
|
||
if temp_domains_cache is None:
|
||
temp_domains_cache = load_temp_domains()
|
||
return domain in temp_domains_cache
|
||
except Exception:
|
||
return False
|
||
|
||
def is_bad_name(name):
|
||
if not name or len(name.strip()) < 3:
|
||
return True
|
||
|
||
name = name.strip()
|
||
lowered = name.lower()
|
||
|
||
# Cecha 1: tylko litery/cyfry i brak słów
|
||
entropy_chars = len(set(name)) / len(name)
|
||
digit_ratio = sum(c.isdigit() for c in name) / len(name)
|
||
alpha_ratio = sum(c.isalpha() for c in name) / len(name)
|
||
has_vowel = any(c in 'aeiouy' for c in lowered)
|
||
has_common_words = any(word in lowered for word in ['admin', 'user', 'test', 'konto', 'login', 'abc', 'name'])
|
||
|
||
# Cecha 2: wygląda na hash/random
|
||
is_alphanum = all(c in string.ascii_letters + string.digits for c in name)
|
||
long_gibberish = is_alphanum and len(name) >= 8 and entropy_chars > 0.6 and digit_ratio > 0.3 and not has_vowel
|
||
|
||
# Punktacja heurystyczna
|
||
score = 0
|
||
if digit_ratio > 0.5: score += 1
|
||
if entropy_chars > 0.7: score += 1
|
||
if not has_vowel: score += 1
|
||
if not has_common_words and is_alphanum and len(name) >= 8: score += 1
|
||
if long_gibberish: score += 1
|
||
if any(c in "!@#$%^&*()" for c in name): score += 1
|
||
if name.lower() in {"unknown", "guest", "null"}: score += 2
|
||
|
||
return score >= 3 # 3+ z 6 wskazuje na losowość
|
||
|
||
def export_to_csv(users):
|
||
now = datetime.now().strftime("%Y-%m-%d_%H%M")
|
||
filename = f"exports/user_cleanup_results_{now}.csv"
|
||
with open(filename, mode='w', newline='', encoding='utf-8') as f:
|
||
writer = csv.writer(f)
|
||
writer.writerow([
|
||
"UID", "Login", "E-mail", "Ostatnie logowanie", "Rejestracja",
|
||
"Punkty", "Nieaktywny", "E-mail OK", "Tymczasowy", "Zły nick"
|
||
])
|
||
for u in users:
|
||
writer.writerow([
|
||
u['uid'],
|
||
u['name'],
|
||
u['mail'],
|
||
datetime.fromtimestamp(u['access']).strftime('%Y-%m-%d') if u['access'] else 'nigdy',
|
||
datetime.fromtimestamp(u['created']).strftime('%Y-%m-%d') if u.get('created') else 'brak',
|
||
u.get('points', 0),
|
||
'TAK' if u.get('inactive') else 'NIE',
|
||
'TAK' if u.get('email_valid') else 'NIE',
|
||
'TAK' if u.get('temp_email') else 'NIE',
|
||
'TAK' if u.get('bad_name') else 'NIE',
|
||
])
|
||
print(f"📁 CSV zapisany: {filename}")
|
||
|
||
|
||
def export_to_excel(users):
|
||
now = datetime.now().strftime("%Y-%m-%d_%H%M")
|
||
filename = f"exports/user_cleanup_results_{now}.xlsx"
|
||
workbook = xlsxwriter.Workbook(filename)
|
||
sheet = workbook.add_worksheet("Wyniki")
|
||
|
||
headers = [
|
||
"UID", "Login", "E-mail", "Ostatnie logowanie", "Rejestracja",
|
||
"Punkty", "Nieaktywny", "E-mail OK", "Tymczasowy", "Zły nick"
|
||
]
|
||
for col, header in enumerate(headers):
|
||
sheet.write(0, col, header)
|
||
|
||
for row_idx, u in enumerate(users, start=1):
|
||
sheet.write(row_idx, 0, u['uid'])
|
||
sheet.write(row_idx, 1, u['name'])
|
||
sheet.write(row_idx, 2, u['mail'])
|
||
sheet.write(row_idx, 3, datetime.fromtimestamp(u['access']).strftime('%Y-%m-%d') if u['access'] else 'nigdy')
|
||
sheet.write(row_idx, 4, datetime.fromtimestamp(u['created']).strftime('%Y-%m-%d') if u.get('created') else 'brak')
|
||
sheet.write(row_idx, 5, u.get('points', 0))
|
||
sheet.write(row_idx, 6, 'TAK' if u.get('inactive') else 'NIE')
|
||
sheet.write(row_idx, 7, 'TAK' if u.get('email_valid') else 'NIE')
|
||
sheet.write(row_idx, 8, 'TAK' if u.get('temp_email') else 'NIE')
|
||
sheet.write(row_idx, 9, 'TAK' if u.get('bad_name') else 'NIE')
|
||
|
||
workbook.close()
|
||
print(f"📁 Excel zapisany: {filename}")
|
||
|
||
|
||
def flush_redis_cache():
|
||
keys = redis_client.keys("mx:*")
|
||
for key in keys:
|
||
redis_client.delete(key)
|
||
print(f"🧹 Redis MX cache wyczyszczony: {len(keys)} wpisów")
|
||
|
||
def domain_report(users):
|
||
domains = defaultdict(int)
|
||
for u in users:
|
||
domain = u['mail'].split('@')[1].lower()
|
||
domains[domain] += 1
|
||
print("\n📊 Raport domen:")
|
||
for domain, count in sorted(domains.items(), key=lambda x: x[1], reverse=True):
|
||
print(f"- {domain}: {count} użytkowników")
|
||
|
||
def delete_user_via_php(uid, drupal_path):
|
||
try:
|
||
result = subprocess.run(
|
||
['php', 'delete_user.php', str(uid), drupal_path],
|
||
capture_output=True, text=True, check=True
|
||
)
|
||
print(result.stdout.strip())
|
||
logging.info(f"PHP delete UID {uid}: {result.stdout.strip()}")
|
||
except subprocess.CalledProcessError as e:
|
||
logging.error(f"Błąd PHP delete UID {uid}: {e.stderr}")
|
||
|
||
def confirm_delete():
|
||
answer = input("❗ Czy na pewno chcesz ZDEZAKTYWOWAĆ użytkowników? [tak/N]: ").strip().lower()
|
||
if answer not in ("tak", "t", "yes", "y"):
|
||
print("❌ Operacja anulowana.")
|
||
sys.exit(0)
|
||
|
||
def days_to_years(days):
|
||
return round(days / 365, 1)
|
||
|
||
def get_smtp_config():
|
||
config = {
|
||
"host": os.getenv("SMTP_HOST"),
|
||
"port": os.getenv("SMTP_PORT"),
|
||
"user": os.getenv("SMTP_USER"),
|
||
"password": os.getenv("SMTP_PASSWORD")
|
||
}
|
||
|
||
if not config["host"] or not config["port"]:
|
||
raise ValueError("❌ Brakuje SMTP_HOST lub SMTP_PORT w .env")
|
||
|
||
return config
|
||
|
||
def send_email_batch(users, smtp_config, mails_per_pack=100, time_per_pack=60, dry_run=False, template_path="mail_template.html"):
|
||
import os
|
||
|
||
#template_path = "mail_template.html"
|
||
if not os.path.exists(template_path):
|
||
print(f"❌ Brak pliku szablonu: {template_path}")
|
||
return
|
||
|
||
try:
|
||
with open(template_path, "r", encoding="utf-8") as f:
|
||
template = f.read()
|
||
except Exception as e:
|
||
print(f"❌ Błąd podczas odczytu szablonu HTML: {e}")
|
||
return
|
||
|
||
try:
|
||
smtp = smtplib.SMTP(smtp_config["host"], int(smtp_config["port"]))
|
||
smtp.ehlo()
|
||
try:
|
||
smtp.starttls()
|
||
smtp.ehlo()
|
||
except Exception:
|
||
pass
|
||
|
||
if smtp_config["user"] and smtp_config["password"]:
|
||
smtp.login(smtp_config["user"], smtp_config["password"])
|
||
except Exception as e:
|
||
print(f"❌ Błąd połączenia z serwerem SMTP: {e}")
|
||
return
|
||
|
||
print(f"📨 Rozpoczynam wysyłkę {len(users)} maili...")
|
||
|
||
for i in tqdm(range(0, len(users), mails_per_pack), desc="Wysyłanie emaili"):
|
||
batch = users[i:i + mails_per_pack]
|
||
for user in batch:
|
||
if dry_run:
|
||
print(f"🔔 [TRYB TESTOWY] Mail do: {user['mail']}")
|
||
continue
|
||
|
||
try:
|
||
last_login = user.get('access', 0) # Domyślnie 0 jeśli brak
|
||
reg_date = user.get('created', 0) # Domyślnie 0 jeśli brak
|
||
msg = MIMEMultipart()
|
||
msg['From'] = f"Unitra-Klub <{smtp_config['user'] or 'unitra@unitraklub.pl'}>"
|
||
msg['To'] = user['mail']
|
||
msg['Subject'] = "Twoje konto w unitraklub.pl"
|
||
|
||
body = template.replace("@user", user.get('name', 'Użytkowniku')) \
|
||
.replace("@rejestracja", datetime.fromtimestamp(reg_date).strftime('%Y-%m-%d') if reg_date else 'nieznana') \
|
||
.replace("@ostatnie_logowanie", datetime.fromtimestamp(last_login).strftime('%Y-%m-%d') if last_login else 'nigdy')
|
||
|
||
msg.attach(MIMEText(body, 'html'))
|
||
|
||
smtp.send_message(msg)
|
||
time.sleep(0.5)
|
||
except Exception as e:
|
||
print(f"⚠️ Błąd wysyłki do {user['mail']}: {e}")
|
||
|
||
if i + mails_per_pack < len(users):
|
||
print(f"⏸ Przerwa {time_per_pack} sekund między paczkami...")
|
||
time.sleep(time_per_pack)
|
||
|
||
smtp.quit()
|
||
print("✅ Wysyłka zakończona.")
|
||
|
||
def validate_smtp_config(config):
|
||
required = ['host', 'port', 'user', 'password']
|
||
for key in required:
|
||
if not config.get(key):
|
||
raise ValueError(f"❌ Brakuje wartości SMTP dla: {key}")
|
||
|
||
def main():
|
||
signal.signal(signal.SIGINT, lambda s, f: sys.exit("\n🛑 Przerwano przez użytkownika."))
|
||
load_dotenv()
|
||
try:
|
||
smtp_config = get_smtp_config()
|
||
except ValueError as e:
|
||
print(str(e))
|
||
sys.exit(1)
|
||
|
||
parser = argparse.ArgumentParser(
|
||
description="Drupal 6 user cleanup tool",
|
||
epilog=epilog,
|
||
formatter_class=argparse.RawDescriptionHelpFormatter
|
||
)
|
||
|
||
parser.add_argument('--host', help='Adres hosta bazy danych (można ustawić w .env jako DB_HOST)')
|
||
parser.add_argument('--user', help='Użytkownik bazy danych (lub DB_USER z .env)')
|
||
parser.add_argument('--password', help='Hasło do bazy danych (lub DB_PASSWORD z .env)')
|
||
parser.add_argument('--database', help='Nazwa bazy danych (lub DB_NAME z .env)')
|
||
|
||
parser.add_argument('--days-inactive', type=int,
|
||
help='Minimalna liczba dni nieaktywności, po której użytkownik uznawany jest za nieaktywny')
|
||
|
||
parser.add_argument('--dry-run', dest='dry_run', action='store_true', help='Włącz tryb testowy')
|
||
parser.add_argument('--no-dry-run', dest='dry_run', action='store_false', help='Wyłącz tryb testowy')
|
||
parser.set_defaults(dry_run=None)
|
||
|
||
parser.add_argument('--delete', action='store_true',
|
||
help='Usuń (dezaktywuj) użytkowników, którzy spełniają kryteria filtrowania')
|
||
|
||
parser.add_argument('--validate', action='store_true',
|
||
help='Tylko sprawdź poprawność adresów e-mail (bez usuwania)')
|
||
|
||
parser.add_argument('--flush-cache', action='store_true',
|
||
help='Wyczyść cache rekordów MX w Redisie')
|
||
|
||
parser.add_argument('--export-excel', action='store_true',
|
||
help='Zapisz wynik filtrowania użytkowników także do pliku Excel (.xlsx)')
|
||
|
||
parser.add_argument('--report-domains', action='store_true',
|
||
help='Wygeneruj raport ilości użytkowników według domen e-mail')
|
||
|
||
parser.add_argument('--veteran-year', type=int, default=2012,
|
||
help='Rok, przed którym konto uznawane jest za stare/weterana (domyślnie: 2012)')
|
||
|
||
parser.add_argument('--recent-login-days', type=int, default=1095,
|
||
help='Ile dni wstecz uznaje się zalogowanego weterana za aktywnego (domyślnie: 1095)')
|
||
|
||
parser.add_argument('--show-table', action='store_true',
|
||
help='Wyświetl tabelę użytkowników spełniających kryteria do usunięcia')
|
||
|
||
parser.add_argument('--drupal-path',
|
||
help='Ścieżka do katalogu Drupala (można ustawić przez .env jako DRUPAL_PATH)')
|
||
|
||
parser.add_argument('--send-test', metavar="EMAIL",
|
||
help='Wyślij testowego maila z szablonu na wskazany adres e-mail')
|
||
|
||
parser.add_argument('--send-mails', action='store_true',
|
||
help='Wyślij powiadomienia e-mail do użytkowników z listy kandydatów')
|
||
|
||
parser.add_argument('--mails-per-pack', type=int, default=100,
|
||
help='Ile e-maili wysyłać w jednej paczce (domyślnie: 100)')
|
||
|
||
parser.add_argument('--time-per-pack', type=int, default=60,
|
||
help='Ile sekund czekać między paczkami maili (domyślnie: 60 sek.)')
|
||
|
||
parser.add_argument('--only-invalid-emails', action='store_true',
|
||
help='Usuń (dezaktywuj) tylko użytkowników z nieprawidłowymi lub tymczasowymi adresami e-mail (bez sprawdzania aktywności)')
|
||
|
||
parser.add_argument("--inactive-since", type=str,
|
||
help="Zakres dni nieaktywności w formacie min-max, np. 360-1825 (tylko dla wysyłki maili)"
|
||
)
|
||
|
||
parser.add_argument('--bad-name', action='store_true',
|
||
help='Przeszukaj bazę użytkowników z losowymi lub bezużytecznymi nazwami')
|
||
|
||
parser.add_argument('--stats', action='store_true',
|
||
help='Wyświetl statystyki: aktywność, rok rejestracji, domeny')
|
||
|
||
parser.add_argument('--mail-template', type=str,
|
||
help='Ścieżka do alternatywnego pliku HTML z szablonem maila (domyślnie: mail_template.html)')
|
||
|
||
parser.add_argument('--search-user', metavar="USERNAME",
|
||
help='Wyszukaj użytkownika po nazwie i wyświetl jego statystyki')
|
||
|
||
args = parser.parse_args()
|
||
|
||
inactive_range = None
|
||
if args.inactive_since:
|
||
try:
|
||
min_days, max_days = map(int, args.inactive_since.split('-'))
|
||
inactive_range = (min_days, max_days)
|
||
except Exception as e:
|
||
print(f"Błąd parsowania zakresu dni: {e}")
|
||
exit(1)
|
||
|
||
if args.send_test:
|
||
test_user = {
|
||
'name': 'Testowy Użytkownik',
|
||
'mail': args.send_test,
|
||
'created': int(datetime.now().timestamp()) - (86400 * 365 * 2)
|
||
}
|
||
print(f"📬 Wysyłka testowego maila na: {test_user['mail']}")
|
||
send_email_batch(
|
||
[test_user],
|
||
smtp_config,
|
||
mails_per_pack=1,
|
||
time_per_pack=0,
|
||
dry_run=False,
|
||
template_path=args.mail_template
|
||
)
|
||
return
|
||
|
||
if not args.drupal_path:
|
||
args.drupal_path = os.getenv("DRUPAL_PATH")
|
||
|
||
if not args.delete and args.dry_run is None:
|
||
args.dry_run = True
|
||
|
||
if args.flush_cache:
|
||
flush_redis_cache()
|
||
return
|
||
|
||
if args.search_user:
|
||
db_config = {
|
||
'host': args.host or os.getenv('DB_HOST'),
|
||
'user': args.user or os.getenv('DB_USER'),
|
||
'password': args.password or os.getenv('DB_PASSWORD'),
|
||
'database': args.database or os.getenv('DB_NAME')
|
||
}
|
||
search_user(db_config, args.search_user)
|
||
return
|
||
|
||
db_config = {
|
||
'host': args.host or os.getenv('DB_HOST'),
|
||
'user': args.user or os.getenv('DB_USER'),
|
||
'password': args.password or os.getenv('DB_PASSWORD'),
|
||
'database': args.database or os.getenv('DB_NAME')
|
||
}
|
||
|
||
users = get_users(db_config)
|
||
now_ts = int(datetime.now().timestamp())
|
||
final_candidates = []
|
||
inactive_count = 0
|
||
invalid_email_count = 0
|
||
temp_email_count = 0
|
||
skipped_with_points = 0
|
||
skipped_veterans = 0
|
||
bad_name_count = 0
|
||
|
||
temp_domains_cache = load_temp_domains()
|
||
|
||
for user in tqdm(users, desc="Analiza"):
|
||
if (user.get('points') or 0) > 0:
|
||
skipped_with_points += 1
|
||
continue
|
||
|
||
if (user.get('post_count') or 0) > 0:
|
||
continue
|
||
|
||
if user.get('bad_name'):
|
||
bad_name_count += 1
|
||
|
||
last_access = user['access'] or 0
|
||
days_inactive = (now_ts - last_access) / 86400
|
||
|
||
# Nowa logika oznaczania nieaktywnych
|
||
is_inactive_by_days = (args.days_inactive is not None) and (days_inactive > args.days_inactive)
|
||
is_inactive_by_range = inactive_range and (inactive_range[0] <= days_inactive <= inactive_range[1])
|
||
|
||
user['inactive'] = is_inactive_by_days or is_inactive_by_range
|
||
user['temp_email'] = is_temp_email(user['mail'], temp_domains_cache)
|
||
user['email_valid'] = not is_fake_email(user['mail']) and not user['temp_email']
|
||
user['bad_name'] = args.bad_name and is_bad_name(user['name'])
|
||
|
||
if args.only_invalid_emails:
|
||
if not user['email_valid']:
|
||
final_candidates.append(user)
|
||
continue
|
||
|
||
created_year = datetime.fromtimestamp(user['created']).year if user.get('created') else None
|
||
recent_login_threshold = now_ts - (args.recent_login_days * 86400)
|
||
|
||
if created_year and created_year <= args.veteran_year:
|
||
if user['access'] and user['access'] >= recent_login_threshold:
|
||
skipped_veterans += 1
|
||
continue
|
||
|
||
if user['inactive']:
|
||
inactive_count += 1
|
||
if not user['email_valid']:
|
||
invalid_email_count += 1
|
||
if user['temp_email']:
|
||
temp_email_count += 1
|
||
|
||
if args.validate or user['inactive'] or not user['email_valid']:
|
||
final_candidates.append(user)
|
||
|
||
final_candidates = [u for u in final_candidates if (u.get('points') or 0) == 0]
|
||
|
||
if args.stats:
|
||
from collections import Counter
|
||
|
||
def bucket_days(days):
|
||
if days < 365: return "<1 rok"
|
||
if days < 730: return "1–2 lata"
|
||
if days < 1095: return "2–3 lata"
|
||
return "3+ lata"
|
||
|
||
access_buckets = Counter()
|
||
created_years = Counter()
|
||
email_domains = Counter()
|
||
|
||
for u in users:
|
||
last = u.get("access") or 0
|
||
days_inactive = (now_ts - last) / 86400
|
||
access_buckets[bucket_days(days_inactive)] += 1
|
||
|
||
if u.get("created"):
|
||
y = datetime.fromtimestamp(u["created"]).year
|
||
created_years[y] += 1
|
||
|
||
try:
|
||
domain = u["mail"].split("@")[1].lower()
|
||
email_domains[domain] += 1
|
||
except:
|
||
pass
|
||
|
||
print("\n📊 Statystyki:")
|
||
print("- Rozkład nieaktywności:")
|
||
for k, v in sorted(access_buckets.items()):
|
||
print(f" {k}: {v}")
|
||
|
||
print("- Rok rejestracji:")
|
||
for k in sorted(created_years):
|
||
print(f" {k}: {created_years[k]}")
|
||
|
||
print("- Top domeny:")
|
||
for dom, cnt in email_domains.most_common(10):
|
||
print(f" {dom}: {cnt}")
|
||
|
||
|
||
if args.report_domains:
|
||
domain_report(final_candidates)
|
||
|
||
if args.show_table:
|
||
print(tabulate([
|
||
[u['uid'], u['name'], u['mail'],
|
||
datetime.fromtimestamp(u['access']).strftime('%Y-%m-%d') if u['access'] else 'nigdy',
|
||
datetime.fromtimestamp(u['created']).strftime('%Y-%m-%d') if u.get('created') else 'brak',
|
||
u.get('points', 0),
|
||
'TAK' if u['inactive'] else 'NIE',
|
||
'TAK' if u['email_valid'] else 'NIE',
|
||
'TAK' if u['temp_email'] else 'NIE']
|
||
for u in final_candidates
|
||
], headers=["UID", "Login", "E-mail", "Ostatnie log.", "Rejestracja", "Punkty", "Nieaktywny", "E-mail OK", "Tymczasowy"], tablefmt="fancy_grid"))
|
||
|
||
export_to_csv(final_candidates)
|
||
|
||
if args.export_excel:
|
||
export_to_excel(final_candidates)
|
||
|
||
if args.send_mails:
|
||
send_email_batch(
|
||
[u for u in final_candidates if u['inactive']],
|
||
smtp_config,
|
||
args.mails_per_pack,
|
||
args.time_per_pack,
|
||
dry_run=args.dry_run,
|
||
template_path=args.mail_template
|
||
)
|
||
|
||
|
||
print("\n📋 Parametry filtrowania:")
|
||
if args.days_inactive:
|
||
print(f"- Nieaktywni: > {args.days_inactive} dni (~{days_to_years(args.days_inactive)} lat)")
|
||
if inactive_range:
|
||
print(f"- Zakres nieaktywności: {inactive_range[0]}-{inactive_range[1]} dni (~{days_to_years(inactive_range[0])}-{days_to_years(inactive_range[1])} lat)")
|
||
|
||
|
||
print(f"- Weterani: konta przed {args.veteran_year}")
|
||
print(f"- Pominięci weterani: logowanie w ostatnich {args.recent_login_days} dniach")
|
||
|
||
print("\n📊 Podsumowanie:")
|
||
print(f"- Całkowita liczba użytkowników: {len(users)}")
|
||
print(f"- Pominięci z punktami: {skipped_with_points}")
|
||
print(f"- Nieaktywni: {inactive_count}")
|
||
print(f"- Z niepoprawnym e-mailem: {invalid_email_count}")
|
||
print(f"- Z tymczasowym e-mailem: {temp_email_count}")
|
||
print(f"- Kandydaci do usunięcia: {len(final_candidates)}")
|
||
print(f"- Pominięci weterani: {skipped_veterans}")
|
||
print(f"- Z losowym lub podejrzanym nickiem: {bad_name_count}")
|
||
|
||
if args.delete:
|
||
confirm_delete()
|
||
if not args.drupal_path:
|
||
print("❌ Brak ścieżki do Drupala!")
|
||
sys.exit(1)
|
||
for u in tqdm(final_candidates, desc="Usuwanie"):
|
||
delete_user_via_php(u['uid'], args.drupal_path)
|
||
print(f"✅ Zedaktywowano {len(final_candidates)} użytkowników")
|
||
|
||
if __name__ == '__main__':
|
||
main()
|