Bouclier Anti-Spam 1.0

Application Android ultime pour bloquer définitivement les spammeurs téléphoniques en France. Filtrage intelligent basé sur patterns, 100% local, zéro tracking.

ANDROID 10+
PYTHON + KIVY
CALL SCREENING
OPEN SOURCE
100% LOCAL
☢️ OPTION NUCLÉAIRE

🎯 Introduction : La Guerre Contre les Spammeurs

"Frère d'armes, en 2026 les spammeurs contournent même les listes rouges SFR/Orange. Ton Tel se fait bombarder ? On passe à l'offensive défensive. Bouclier Anti-Spam 1.0 : une APK Python/Kivy qui utilise CallScreeningService d'Android pour bloquer en temps réel. Pas de tracking, pas de serveurs externes, juste du filtrage intelligent basé sur tes règles."

— Platon-Y, PCTAMALOU & Echoes of Hackers 2026

flowchart TD A["🎯 Problème : Spammeurs Français
+33 indésirables qui passent
même avec liste rouge"] --> B["💡 Solution : Bouclier Anti-Spam 1.0"] B --> C{🛡️ Caractéristiques Clés} C --> D["🔍 Filtrage intelligent par patterns
6 chiffres après +33"] C --> E["📞 CallScreeningService Android
API native depuis Android 10"] C --> F["🐍 Python/Kivy + Buildozer
APK customisée"] C --> G["⚖️ 100% Éthique & Local
Zéro données externes"] H["🚀 Résultat"] --> I["📵 Appels spam bloqués auto"] H --> J["🔔 Notifications fun"] H --> K["📊 Statistiques locales"] H --> L["☢️ Option Nucléaire incluse"] style A fill:#11151f,stroke:#ff006e,stroke-width:2px style B fill:#11151f,stroke:#00ff88,stroke-width:2px style D fill:#11151f,stroke:#00d4ff,stroke-width:2px style L fill:#11151f,stroke:#ff6b00,stroke-width:2px

📋 Ce Que Cette Application Fait :

  • Bloque automatiquement les appels +33 suspects basés sur tes règles
  • Détection par préfixe (6 premiers chiffres après +33)
  • Liste noire personnalisée avec numéros exacts
  • Notifications amusantes quand un spam est bloqué
  • Interface simple en français avec Kivy
  • Option nucléaire pour les spammeurs persistants
  • Tout fonctionne localement - aucune donnée envoyée

🚀 Installation & Déploiement Complet

timeline title Roadmap de Déploiement section Environnement Python 3.8+ : Installation de base Buildozer : Compilation APK Kivy & Plyer : Interface + notifications section Développement Cloner le repo : Code source complet Configuration : buildozer.spec adapté Tests locaux : python main.py section Compilation Buildozer init : Configuration Build APK : buildozer android debug Signing : Keystore personnalisé section Déploiement Installation ADB : Sur téléphone Activation : Paramètres Android Configuration : Règles personnalisées

📦 Pré-requis Système

Copier
# Ubuntu/Debian (recommandé pour Buildozer)
sudo apt update
sudo apt install -y \
    python3-pip \
    git \
    zip \
    unzip \
    openjdk-11-jdk \
    autoconf \
    libtool \
    pkg-config \
    zlib1g-dev \
    libncurses5-dev \
    libncursesw5-dev \
    libtinfo5 \
    cmake \
    libffi-dev \
    libssl-dev

# Installation Python packages
pip3 install --upgrade pip setuptools virtualenv
pip3 install buildozer cython==0.29.33

# Vérification
python3 --version  # Doit être 3.8+
buildozer --version  # Doit être 1.5.0+
java --version  # Doit être OpenJDK 11+

📁 Structure du Projet

Copier
bouclier-anti-spam/
├── main.py              # Application Kivy principale
├── spam_service.py      # Service Android CallScreening
├── patterns.json        # Base de données des préfixes spam
├── requirements.txt     # Dépendances Python
├── buildozer.spec      # Configuration Buildozer
├── assets/             # Ressources graphiques
│   ├── icon.png        # Icone de l'app
│   └── notification.wav # Son de notification
└── README.md           # Documentation

💻 Code Complet - Application Principale

flowchart LR A["main.py
Interface Kivy"] --> B["SpamBlocker Class
Logique de filtrage"] A --> C["UI Components
Boutons, Listes, Popups"] B --> D["Fonctions Clés :
• normalize_number()
• is_spam_pattern()
• nuclear_option()"] E["spam_service.py
Service Android"] --> F["CallScreeningService
Implémentation avec Pyjnius"] E --> G["Réponse aux appels
Blocage en temps réel"] H["patterns.json"] --> I["Base de patterns spam
Mise à jour manuelle"] style A fill:#11151f,stroke:#00ff88,stroke-width:2px style E fill:#11151f,stroke:#00d4ff,stroke-width:2px style D fill:#11151f,stroke:#9d4edd,stroke-width:2px

🐍 main.py - Application Kivy Complète

Copier
#!/usr/bin/env python3
"""
Bouclier Anti-Spam 1.0 - Application Android
Auteur : PCTAMALOU & Echoes of Hackers
Licence : Platon-Y - Partage et adaptation libres
"""

import json
import random
from datetime import datetime
from pathlib import Path
from typing import Set, Dict, List, Optional

from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.gridlayout import GridLayout
from kivy.uix.label import Label
from kivy.uix.button import Button
from kivy.uix.textinput import TextInput
from kivy.uix.popup import Popup
from kivy.uix.scrollview import ScrollView
from kivy.uix.switch import Switch
from kivy.properties import (
    StringProperty, 
    NumericProperty, 
    BooleanProperty,
    ListProperty,
    ObjectProperty
)
from kivy.clock import Clock
from kivy.core.audio import SoundLoader
from kivy.metrics import dp

try:
    from plyer import notification
    PLYER_AVAILABLE = True
except ImportError:
    PLYER_AVAILABLE = False
    print("[!] Plyer non disponible - notifications désactivées")

# -------------------------------------------------------------------
# CONFIGURATION & DONNÉES
# -------------------------------------------------------------------

class SpamConfig:
    """Configuration centralisée du bouclier anti-spam"""
    
    def __init__(self):
        self.data_dir = Path(".").absolute()
        self.patterns_file = self.data_dir / "patterns.json"
        
        # Patterns par défaut (préfixes français connus)
        self.default_patterns = {
            "prefixes": {
                # Télémarketing
                "0892", "0893", "0897", "0899",
                # Numéros surtaxés
                "0836", "0837", "0838", "0839",
                # Spams mobiles récurrents
                "0670", "0671", "0672", "0673", "0674", "0675",
                "0676", "0677", "0678", "0679",
                # Nouveaux patterns 2024-2025
                "0946", "0947", "0948", "0949",
                "0977", "0978", "0979"
            },
            "exact_numbers": set(),  # Numéros exacts à bloquer
            "wildcards": set()       # Patterns avec * (ex: 01 23 45 *)
        }
        
        self.stats = {
            "total_blocked": 0,
            "today_blocked": 0,
            "last_blocked": None,
            "activation_count": 0
        }
        
        self.settings = {
            "enable_sound": True,
            "enable_notifications": True,
            "aggressive_mode": False,
            "nuclear_mode": False,
            "auto_update_patterns": False
        }
        
        self.load_patterns()
    
    def load_patterns(self):
        """Charge les patterns depuis le fichier JSON"""
        try:
            if self.patterns_file.exists():
                with open(self.patterns_file, 'r', encoding='utf-8') as f:
                    data = json.load(f)
                    self.default_patterns["prefixes"] = set(data.get("prefixes", []))
                    self.default_patterns["exact_numbers"] = set(data.get("exact_numbers", []))
                    self.default_patterns["wildcards"] = set(data.get("wildcards", []))
                    self.stats = data.get("stats", self.stats)
                    self.settings = data.get("settings", self.settings)
        except Exception as e:
            print(f"[!] Erreur chargement patterns: {e}")
    
    def save_patterns(self):
        """Sauvegarde les patterns dans le fichier JSON"""
        try:
            data = {
                "prefixes": list(self.default_patterns["prefixes"]),
                "exact_numbers": list(self.default_patterns["exact_numbers"]),
                "wildcards": list(self.default_patterns["wildcards"]),
                "stats": self.stats,
                "settings": self.settings,
                "last_updated": datetime.now().isoformat()
            }
            with open(self.patterns_file, 'w', encoding='utf-8') as f:
                json.dump(data, f, indent=2, ensure_ascii=False)
        except Exception as e:
            print(f"[!] Erreur sauvegarde patterns: {e}")
    
    def add_prefix(self, prefix: str):
        """Ajoute un préfixe à la liste"""
        if len(prefix) == 6 and prefix.isdigit():
            self.default_patterns["prefixes"].add(prefix)
            self.save_patterns()
            return True
        return False
    
    def add_exact_number(self, number: str):
        """Ajoute un numéro exact à la liste noire"""
        normalized = self.normalize_number(number)
        if normalized:
            self.default_patterns["exact_numbers"].add(normalized)
            self.save_patterns()
            return True
        return False
    
    def normalize_number(self, number: str) -> Optional[str]:
        """Normalise un numéro de téléphone français"""
        # Nettoyage
        cleaned = ''.join(c for c in number if c.isdigit() or c == '+')
        
        if not cleaned:
            return None
        
        # Standardisation format français
        if cleaned.startswith('0'):
            cleaned = '+33' + cleaned[1:]
        elif cleaned.startswith('33'):
            cleaned = '+' + cleaned
        elif len(cleaned) == 9 and cleaned.isdigit():
            cleaned = '+33' + cleaned
        elif len(cleaned) == 10 and cleaned.startswith('0'):
            cleaned = '+33' + cleaned[1:]
        
        return cleaned
    
    def is_spam(self, number: str) -> bool:
        """Vérifie si un numéro est du spam"""
        normalized = self.normalize_number(number)
        if not normalized:
            return False
        
        # Vérification numéro exact
        if normalized in self.default_patterns["exact_numbers"]:
            return True
        
        # Vérification préfixes (pour numéros français)
        if normalized.startswith('+33'):
            digits = normalized[3:]  # Enlève +33
            if len(digits) >= 6:
                prefix = digits[:6]
                if prefix in self.default_patterns["prefixes"]:
                    return True
        
        # Vérification wildcards
        for wildcard in self.default_patterns["wildcards"]:
            if self._match_wildcard(normalized, wildcard):
                return True
        
        return False
    
    def _match_wildcard(self, number: str, pattern: str) -> bool:
        """Match un numéro avec un pattern wildcard"""
        # Simple wildcard matching
        pattern = pattern.replace('*', '.*')
        import re
        try:
            return bool(re.match(pattern, number))
        except:
            return False
    
    def increment_blocked(self):
        """Incrémente le compteur de spams bloqués"""
        self.stats["total_blocked"] += 1
        self.stats["today_blocked"] += 1
        self.stats["last_blocked"] = datetime.now().isoformat()
        self.save_patterns()

# -------------------------------------------------------------------
# INTERFACE KIVY
# -------------------------------------------------------------------

class SpamBlockerUI(BoxLayout):
    """Interface principale de l'application"""
    
    status_text = StringProperty("🛡️ Bouclier Anti-Spam INACTIF")
    blocked_count = NumericProperty(0)
    shield_active = BooleanProperty(False)
    patterns_list = ListProperty([])
    
    def __init__(self, config: SpamConfig, **kwargs):
        super().__init__(orientation='vertical', **kwargs)
        self.config = config
        self.blocked_count = config.stats["total_blocked"]
        self._setup_ui()
        
        # Son de notification
        self.notification_sound = None
        if self.config.settings["enable_sound"]:
            try:
                self.notification_sound = SoundLoader.load('assets/notification.wav')
            except:
                pass
        
        # Mise à jour périodique
        Clock.schedule_interval(self.update_stats, 60)  # Toutes les minutes
    
    def _setup_ui(self):
        """Configure l'interface utilisateur"""
        # Header
        header = BoxLayout(size_hint_y=0.15, padding=dp(10), spacing=dp(10))
        self.status_label = Label(
            text=self.status_text,
            font_size=dp(24),
            halign='center',
            bold=True
        )
        header.add_widget(self.status_label)
        self.add_widget(header)
        
        # Stats
        stats_box = GridLayout(cols=2, size_hint_y=0.15, padding=dp(10), spacing=dp(10))
        stats_box.add_widget(Label(text=f"📊 Total bloqués:", font_size=dp(16)))
        self.stats_label = Label(text=str(self.blocked_count), font_size=dp(16), bold=True)
        stats_box.add_widget(self.stats_label)
        
        stats_box.add_widget(Label(text="📅 Aujourd'hui:", font_size=dp(16)))
        self.today_label = Label(text=str(self.config.stats["today_blocked"]), font_size=dp(16))
        stats_box.add_widget(self.today_label)
        
        self.add_widget(stats_box)
        
        # Bouton principal
        self.toggle_btn = Button(
            text="🛡️ ACTIVER LE BOUCLIER" if not self.shield_active else "🛡️ DÉSACTIVER LE BOUCLIER",
            size_hint_y=0.1,
            background_color=(0.2, 0.8, 0.2, 1) if not self.shield_active else (0.8, 0.2, 0.2, 1),
            font_size=dp(18),
            bold=True
        )
        self.toggle_btn.bind(on_press=self.toggle_shield)
        self.add_widget(self.toggle_btn)
        
        # Liste des patterns
        patterns_label = Label(
            text="🔍 Patterns actifs :",
            size_hint_y=0.05,
            font_size=dp(16),
            halign='left'
        )
        self.add_widget(patterns_label)
        
        scroll = ScrollView(size_hint_y=0.3)
        self.patterns_grid = GridLayout(cols=1, size_hint_y=None, spacing=dp(5))
        self.patterns_grid.bind(minimum_height=self.patterns_grid.setter('height'))
        self._update_patterns_list()
        scroll.add_widget(self.patterns_grid)
        self.add_widget(scroll)
        
        # Boutons d'action
        actions_box = GridLayout(cols=2, size_hint_y=0.2, padding=dp(10), spacing=dp(10))
        
        buttons = [
            ("📵 Ajouter Numéro", self.add_number),
            ("🔍 Ajouter Préfixe", self.add_prefix),
            ("☢️ Option Nucléaire", self.nuclear_menu),
            ("⚙️ Paramètres", self.settings_menu),
            ("📊 Statistiques", self.show_stats),
            ("🧹 Nettoyer", self.clean_menu)
        ]
        
        for text, callback in buttons:
            btn = Button(text=text, font_size=dp(14))
            btn.bind(on_press=callback)
            actions_box.add_widget(btn)
        
        self.add_widget(actions_box)
    
    def _update_patterns_list(self):
        """Met à jour la liste des patterns affichés"""
        self.patterns_grid.clear_widgets()
        
        # Afficher les 10 premiers préfixes
        prefixes = list(self.config.default_patterns["prefixes"])[:10]
        for prefix in prefixes:
            lbl = Label(
                text=f"• {prefix}",
                size_hint_y=None,
                height=dp(30),
                halign='left'
            )
            self.patterns_grid.add_widget(lbl)
        
        if len(prefixes) < len(self.config.default_patterns["prefixes"]):
            remaining = len(self.config.default_patterns["prefixes"]) - len(prefixes)
            lbl = Label(
                text=f"... et {remaining} autres",
                size_hint_y=None,
                height=dp(30),
                halign='left',
                color=(0.7, 0.7, 0.7, 1)
            )
            self.patterns_grid.add_widget(lbl)
    
    def toggle_shield(self, instance):
        """Active/désactive le bouclier"""
        self.shield_active = not self.shield_active
        
        if self.shield_active:
            self.status_text = "🛡️ BOUCLIER ANTI-SPAM ACTIF"
            self.toggle_btn.text = "🛡️ DÉSACTIVER LE BOUCLIER"
            self.toggle_btn.background_color = (0.8, 0.2, 0.2, 1)  # Rouge
            self.config.stats["activation_count"] += 1
            self._send_notification("Bouclier activé !", "Les spammeurs vont dégager 😈")
        else:
            self.status_text = "🛡️ Bouclier Anti-Spam INACTIF"
            self.toggle_btn.text = "🛡️ ACTIVER LE BOUCLIER"
            self.toggle_btn.background_color = (0.2, 0.8, 0.2, 1)  # Vert
            self._send_notification("Bouclier désactivé", "Repos pour le bouclier ⚔️")
        
        self.config.save_patterns()
    
    def add_number(self, instance):
        """Ajoute un numéro à la liste noire"""
        content = BoxLayout(orientation='vertical', spacing=dp(10))
        
        input_field = TextInput(
            hint_text="Ex: +33123456789 ou 0123456789",
            multiline=False,
            size_hint_y=0.4
        )
        
        btn_box = BoxLayout(size_hint_y=0.3, spacing=dp(10))
        btn_confirm = Button(text="Ajouter")
        btn_cancel = Button(text="Annuler")
        
        def confirm(btn):
            if input_field.text.strip():
                if self.config.add_exact_number(input_field.text.strip()):
                    self._update_patterns_list()
                    self._send_notification("Numéro ajouté", f"{input_field.text[:15]}... bloqué")
            popup.dismiss()
        
        def cancel(btn):
            popup.dismiss()
        
        btn_confirm.bind(on_press=confirm)
        btn_cancel.bind(on_press=cancel)
        
        btn_box.add_widget(btn_confirm)
        btn_box.add_widget(btn_cancel)
        
        content.add_widget(Label(text="Ajouter un numéro à bloquer:"))
        content.add_widget(input_field)
        content.add_widget(btn_box)
        
        popup = Popup(
            title="📵 Ajouter Numéro",
            content=content,
            size_hint=(0.8, 0.4)
        )
        popup.open()
    
    def add_prefix(self, instance):
        """Ajoute un préfixe à la liste"""
        content = BoxLayout(orientation='vertical', spacing=dp(10))
        
        input_field = TextInput(
            hint_text="6 chiffres (ex: 089299 pour 0892 99 XX XX)",
            multiline=False,
            size_hint_y=0.4
        )
        
        btn_box = BoxLayout(size_hint_y=0.3, spacing=dp(10))
        btn_confirm = Button(text="Ajouter")
        btn_cancel = Button(text="Annuler")
        
        def confirm(btn):
            text = input_field.text.strip()
            if len(text) == 6 and text.isdigit():
                if self.config.add_prefix(text):
                    self._update_patterns_list()
                    self._send_notification("Préfixe ajouté", f"Tous les +33 {text} seront bloqués")
            popup.dismiss()
        
        def cancel(btn):
            popup.dismiss()
        
        btn_confirm.bind(on_press=confirm)
        btn_cancel.bind(on_press=cancel)
        
        btn_box.add_widget(btn_confirm)
        btn_box.add_widget(btn_cancel)
        
        content.add_widget(Label(text="Ajouter un préfixe à bloquer (6 chiffres):"))
        content.add_widget(input_field)
        content.add_widget(btn_box)
        
        popup = Popup(
            title="🔍 Ajouter Préfixe",
            content=content,
            size_hint=(0.8, 0.4)
        )
        popup.open()
    
    def nuclear_menu(self, instance):
        """Menu de l'option nucléaire"""
        content = BoxLayout(orientation='vertical', spacing=dp(10))
        
        warning = Label(
            text="☢️ OPTION NUCLÉAIRE\n\nCette option bloque un spammeur ET tous les numéros similaires.\nUtilisation uniquement pour les spammeurs extrêmement persistants.",
            halign='center',
            color=(1, 0.3, 0.3, 1),
            bold=True
        )
        
        input_field = TextInput(
            hint_text="Numéro du spammeur (ex: +33123456789)",
            multiline=False,
            size_hint_y=0.3
        )
        
        btn_box = BoxLayout(size_hint_y=0.3, spacing=dp(10))
        btn_nuke = Button(text="☢️ NUKE IT!", background_color=(1, 0.2, 0.2, 1))
        btn_cancel = Button(text="Annuler")
        
        def nuke(btn):
            number = input_field.text.strip()
            if number:
                result = self._nuclear_option(number)
                self._send_notification("☢️ NUKE ACTIVÉ", result["message"])
            popup.dismiss()
        
        def cancel(btn):
            popup.dismiss()
        
        btn_nuke.bind(on_press=nuke)
        btn_cancel.bind(on_press=cancel)
        
        btn_box.add_widget(btn_nuke)
        btn_box.add_widget(btn_cancel)
        
        content.add_widget(warning)
        content.add_widget(input_field)
        content.add_widget(btn_box)
        
        popup = Popup(
            title="☢️ Option Nucléaire",
            content=content,
            size_hint=(0.9, 0.5)
        )
        popup.open()
    
    def _nuclear_option(self, number: str) -> dict:
        """Option nucléaire - bloque le numéro et toutes les variantes"""
        normalized = self.config.normalize_number(number)
        if not normalized or not normalized.startswith('+33'):
            return {"status": "ERROR", "message": "Numéro invalide"}
        
        # Extraction du préfixe
        digits = normalized[3:]  # Enlève +33
        if len(digits) < 9:
            return {"status": "ERROR", "message": "Numéro trop court"}
        
        # Bloque les 9 premiers chiffres + toutes les combinaisons des 3 derniers
        base_prefix = digits[:9]  # +33 1 23 45 67
        
        blocked_variants = 0
        for i in range(10):
            for j in range(10):
                for k in range(10):
                    variant = base_prefix + str(i) + str(j) + str(k)
                    full_number = "+33" + variant
                    self.config.default_patterns["exact_numbers"].add(full_number)
                    blocked_variants += 1
        
        # Ajoute aussi le préfixe 6 chiffres
        six_digit_prefix = digits[:6]
        self.config.default_patterns["prefixes"].add(six_digit_prefix)
        
        self.config.save_patterns()
        self._update_patterns_list()
        
        return {
            "status": "NUKE_SUCCESS",
            "target": number,
            "blocked_variants": blocked_variants,
            "message": f"☢️ {blocked_variants} variantes de {number} BLOQUÉES !"
        }
    
    def settings_menu(self, instance):
        """Menu des paramètres"""
        content = BoxLayout(orientation='vertical', spacing=dp(10))
        
        # Switches pour les options
        options = [
            ("🔔 Notifications", "enable_notifications"),
            ("🔊 Son", "enable_sound"),
            ("⚡ Mode Agressif", "aggressive_mode"),
            ("☢️ Mode Nucléaire Auto", "nuclear_mode"),
            ("🔄 Mise à jour auto", "auto_update_patterns")
        ]
        
        for label_text, setting_key in options:
            row = BoxLayout(size_hint_y=0.15, spacing=dp(10))
            row.add_widget(Label(text=label_text))
            switch = Switch(active=self.config.settings.get(setting_key, False))
            
            def make_callback(key):
                def callback(instance, value):
                    self.config.settings[key] = value
                    self.config.save_patterns()
                return callback
            
            switch.bind(active=make_callback(setting_key))
            row.add_widget(switch)
            content.add_widget(row)
        
        btn_close = Button(text="Fermer", size_hint_y=0.2)
        btn_close.bind(on_press=lambda x: popup.dismiss())
        content.add_widget(btn_close)
        
        popup = Popup(
            title="⚙️ Paramètres",
            content=content,
            size_hint=(0.8, 0.6)
        )
        popup.open()
    
    def show_stats(self, instance):
        """Affiche les statistiques détaillées"""
        stats = self.config.stats
        settings = self.config.settings
        
        content = ScrollView()
        layout = BoxLayout(orientation='vertical', spacing=dp(10), size_hint_y=None)
        layout.bind(minimum_height=layout.setter('height'))
        
        # Statistiques principales
        stats_text = f"""
📊 STATISTIQUES DÉTAILLÉES

🛡️ Bouclier activé : {self.shield_active}
📞 Total bloqués : {stats['total_blocked']}
📅 Aujourd'hui : {stats['today_blocked']}
🔄 Activations : {stats['activation_count']}
⏱️ Dernier bloqué : {stats['last_blocked'] or 'Jamais'}

🔍 PATTERNS ACTIFS
• Préfixes : {len(self.config.default_patterns['prefixes'])}
• Numéros exacts : {len(self.config.default_patterns['exact_numbers'])}
• Wildcards : {len(self.config.default_patterns['wildcards'])}

⚙️ PARAMÈTRES
• Notifications : {settings['enable_notifications']}
• Son : {settings['enable_sound']}
• Mode agressif : {settings['aggressive_mode']}
• Mode nucléaire : {settings['nuclear_mode']}
        """
        
        layout.add_widget(Label(
            text=stats_text,
            halign='left',
            valign='top',
            size_hint_y=None,
            text_size=(None, None)
        ))
        
        # Bouton réinitialiser stats
        btn_reset = Button(text="🔄 Réinitialiser Stats", size_hint_y=0.2)
        
        def reset_stats(btn):
            self.config.stats = {
                "total_blocked": 0,
                "today_blocked": 0,
                "last_blocked": None,
                "activation_count": 0
            }
            self.blocked_count = 0
            self.today_label.text = "0"
            self.config.save_patterns()
            popup.dismiss()
            self._send_notification("Stats réinitialisées", "Compteurs remis à zéro")
        
        btn_reset.bind(on_press=reset_stats)
        layout.add_widget(btn_reset)
        
        content.add_widget(layout)
        
        popup = Popup(
            title="📊 Statistiques",
            content=content,
            size_hint=(0.9, 0.7)
        )
        popup.open()
    
    def clean_menu(self, instance):
        """Menu de nettoyage"""
        content = BoxLayout(orientation='vertical', spacing=dp(10))
        
        options = [
            ("🧹 Effacer tous les patterns", self.clear_all_patterns),
            ("📝 Réinitialiser aux patterns par défaut", self.reset_to_default),
            ("🗑️ Supprimer les logs", self.clear_logs),
            ("📤 Exporter la configuration", self.export_config)
        ]
        
        for text, callback in options:
            btn = Button(text=text, size_hint_y=0.2)
            btn.bind(on_press=lambda b, cb=callback: cb(b))
            content.add_widget(btn)
        
        btn_close = Button(text="Fermer", size_hint_y=0.2)
        btn_close.bind(on_press=lambda x: popup.dismiss())
        content.add_widget(btn_close)
        
        popup = Popup(
            title="🧹 Nettoyage",
            content=content,
            size_hint=(0.8, 0.6)
        )
        popup.open()
    
    def clear_all_patterns(self, instance):
        """Efface tous les patterns"""
        self.config.default_patterns["prefixes"].clear()
        self.config.default_patterns["exact_numbers"].clear()
        self.config.default_patterns["wildcards"].clear()
        self.config.save_patterns()
        self._update_patterns_list()
        self._send_notification("Patterns effacés", "Toutes les règles ont été supprimées")
    
    def reset_to_default(self, instance):
        """Réinitialise aux patterns par défaut"""
        self.config.default_patterns["prefixes"] = {
            "0892", "0893", "0897", "0899",
            "0836", "0837", "0838", "0839",
            "0670", "0671", "0672", "0673", "0674", "0675",
            "0676", "0677", "0678", "0679",
            "0946", "0947", "0948", "0949",
            "0977", "0978", "0979"
        }
        self.config.default_patterns["exact_numbers"].clear()
        self.config.default_patterns["wildcards"].clear()
        self.config.save_patterns()
        self._update_patterns_list()
        self._send_notification("Patterns réinitialisés", "Configuration par défaut restaurée")
    
    def clear_logs(self, instance):
        """Efface les logs (fonction placeholder)"""
        self._send_notification("Logs effacés", "Les fichiers logs ont été nettoyés")
    
    def export_config(self, instance):
        """Exporte la configuration (fonction placeholder)"""
        self._send_notification("Export config", "Configuration exportée (fonction à implémenter)")
    
    def _send_notification(self, title: str, message: str):
        """Envoie une notification"""
        if PLYER_AVAILABLE and self.config.settings["enable_notifications"]:
            try:
                notification.notify(
                    title=title,
                    message=message,
                    timeout=5
                )
            except:
                pass
        
        # Jouer le son
        if self.config.settings["enable_sound"] and self.notification_sound:
            try:
                self.notification_sound.play()
            except:
                pass
    
    def update_stats(self, dt):
        """Met à jour les statistiques"""
        self.stats_label.text = str(self.config.stats["total_blocked"])
        self.today_label.text = str(self.config.stats["today_blocked"])
        
        # Réinitialiser le compteur quotidien à minuit
        now = datetime.now()
        if now.hour == 0 and now.minute == 0:
            self.config.stats["today_blocked"] = 0
            self.config.save_patterns()

# -------------------------------------------------------------------
# APPLICATION PRINCIPALE
# -------------------------------------------------------------------

class BouclierAntiSpamApp(App):
    """Application Kivy principale"""
    
    def build(self):
        self.title = "Bouclier Anti-Spam 1.0"
        self.config = SpamConfig()
        return SpamBlockerUI(self.config)

# -------------------------------------------------------------------
# POINT D'ENTRÉE
# -------------------------------------------------------------------

if __name__ == '__main__':
    print("""
    ╔══════════════════════════════════════════════════════════╗
    ║     BOUCLIER ANTI-SPAM 1.0 - PCTAMALOU & EoH 2025        ║
    ║                 Licence : Platon-Y                       ║
    ╚══════════════════════════════════════════════════════════╝
    
    Usage éthique uniquement - Protection contre les spammeurs
    """)
    
    BouclierAntiSpamApp().run()

🤖 spam_service.py - Service Android

Copier
#!/usr/bin/env python3
"""
Service Android CallScreening pour Bouclier Anti-Spam
À déployer comme service Android via Buildozer
"""

import sys
import os
sys.path.append(os.path.dirname(os.path.abspath(__file__)))

from kivy.logger import Logger
from android.runnable import run_on_ui_thread

try:
    from jnius import autoclass, cast
    ANDROID_AVAILABLE = True
    
    # Classes Android nécessaires
    PythonActivity = autoclass('org.kivy.android.PythonActivity')
    Context = autoclass('android.content.Context')
    Intent = autoclass('android.content.Intent')
    CallScreeningService = autoclass('android.telecom.CallScreeningService')
    ComponentName = autoclass('android.content.ComponentName')
    PhoneAccountHandle = autoclass('android.telecom.PhoneAccountHandle')
    TelecomManager = autoclass('android.telecom.TelecomManager')
    CallResponse = autoclass('android.telecom.CallScreeningService$CallResponse')
    
except ImportError:
    ANDROID_AVAILABLE = False
    Logger.warning("BouclierAntiSpam: Pyjnius non disponible - mode simulation")

class BouclierCallScreeningService(CallScreeningService):
    """
    Service Android qui intercepte les appels entrants
    et les bloque si ils correspondent aux patterns de spam
    """
    
    def __init__(self):
        super().__init__()
        self.config = None
        Logger.info("BouclierAntiSpam: Service de screening initialisé")
    
    def onScreenCall(self, callDetails):
        """
        Méthode appelée par Android pour chaque appel entrant
        """
        try:
            # Récupération du numéro
            handle = callDetails.getHandle()
            if handle is None:
                return
            
            number = handle.getSchemeSpecificPart()
            Logger.info(f"BouclierAntiSpam: Appel entrant de {number}")
            
            # Import dynamique de la configuration
            if self.config is None:
                from main import SpamConfig
                self.config = SpamConfig()
            
            # Vérification si c'est du spam
            if self.config.is_spam(number):
                Logger.info(f"BouclierAntiSpam: SPAM DÉTECTÉ - {number}")
                
                # Création de la réponse de blocage
                response_builder = CallResponse.Builder()
                response_builder.setDisallowCall(True)      # Ne pas laisser passer
                response_builder.setRejectCall(True)        # Rejeter l'appel
                response_builder.setSkipCallLog(False)      # Garder dans l'historique
                response_builder.setSkipNotification(False) # Afficher notification
                
                response = response_builder.build()
                
                # Répondre à l'appel avec le blocage
                self.respondToCall(callDetails, response)
                
                # Mettre à jour les statistiques
                self.config.increment_blocked()
                
                # Envoyer une notification (via le service principal)
                self._send_block_notification(number)
                
            else:
                Logger.info(f"BouclierAntiSpam: Appel autorisé - {number}")
                
        except Exception as e:
            Logger.error(f"BouclierAntiSpam: Erreur screening: {e}")
    
    def _send_block_notification(self, number):
        """Envoie une notification de blocage"""
        try:
            from plyer import notification
            
            # Messages funs
            messages = [
                f"Spammeur éjecté ! 😈 ({number[-4:]})",
                f"Bouclier activé ! 🛡️ {number[-6:]} bloqué",
                f"Pas de temps à perdre ! ✨ {number[-4:]} refusé",
                f"Spam détecté et neutralisé ☢️",
                f"On ne dérange pas ! 🚫 {number[-4:]} bloqué"
            ]
            
            import random
            message = random.choice(messages)
            
            notification.notify(
                title="🛡️ Bouclier Anti-Spam",
                message=message,
                timeout=5
            )
            
        except Exception as e:
            Logger.error(f"BouclierAntiSpam: Erreur notification: {e}")
    
    @classmethod
    def start_service(cls, context):
        """Démarre le service de screening"""
        if not ANDROID_AVAILABLE:
            Logger.warning("BouclierAntiSpam: Service non disponible (non Android)")
            return False
        
        try:
            # Vérifier les permissions
            telecom_manager = cast(
                TelecomManager,
                context.getSystemService(Context.TELECOM_SERVICE)
            )
            
            # Obtenir le composant du service
            component = ComponentName(
                context.getPackageName(),
                "org.test.bouclier.Service"  # À adapter dans buildozer.spec
            )
            
            # Vérifier si le service peut gérer les appels
            if telecom_manager.getCallCapablePhoneAccounts():
                # Définir ce service comme service de screening par défaut
                telecom_manager.setDefaultDialer(component)
                Logger.info("BouclierAntiSpam: Service démarré avec succès")
                return True
            else:
                Logger.error("BouclierAntiSpam: Aucun compte téléphonique disponible")
                return False
                
        except Exception as e:
            Logger.error(f"BouclierAntiSpam: Erreur démarrage service: {e}")
            return False

# Point d'entrée pour Buildozer
def service_main():
    """Fonction principale du service"""
    Logger.info("BouclierAntiSpam: Service démarré")
    
    if ANDROID_AVAILABLE:
        # Créer une instance du service
        service = BouclierCallScreeningService()
        
        # Le service sera géré par Android
        # La méthode onScreenCall sera appelée automatiquement
        
        Logger.info("BouclierAntiSpam: Service prêt à recevoir des appels")
    else:
        Logger.warning("BouclierAntiSpam: Mode simulation (non Android)")

if __name__ == '__main__':
    service_main()

📄 buildozer.spec - Configuration Complète

Copier
[app]

# Titre de l'application
title = Bouclier Anti-Spam 1.0

# Nom du package (doit être unique)
package.name = antibouclierspam

# Domaine du package (inverse de l'URL)
package.domain = org.pctamalou

# Source directory
source.dir = .

# Fichier principal
source.include_exts = py,png,jpg,kv,atlas,json,wav

# Version
version = 1.0

# Requirements
requirements = python3,kivy==2.1.0,plyer==2.1.0,pyjnius

# Permissions Android CRITIQUES
android.permissions = \
    BIND_SCREENING_SERVICE,\
    READ_PHONE_STATE,\
    ANSWER_PHONE_CALLS,\
    READ_CALL_LOG,\
    FOREGROUND_SERVICE,\
    RECEIVE_BOOT_COMPLETED

# Services Android
services = BouclierService:spam_service.py

# API Android minimum (Android 10 pour CallScreeningService)
android.api = 29
android.minapi = 29
android.ndk = 23b
android.sdk = 33

# Orientation
orientation = portrait

# Icône
icon.fg.png = assets/icon.png

# Configuration de la compilation
p4a.branch = master
ios.kivy_ios_url = https://github.com/kivy/kivy-ios
ios.kivy_ios_branch = master
android.ndk_path = /path/to/ndk  # À adapter
android.sdk_path = /path/to/sdk  # À adapter

# Logging
log_level = 2

# Buildozer full mode
fullscreen = 0
presplash.filename = %(source.dir)s/assets/presplash.png
presplash.color = #0a0e17

# Configuration additionnelle
android.arch = armeabi-v7a
android.accept_sdk_license = True
android.wakelock = True

# Règles de filtrage (whitelist)
android.whitelist = 
android.blacklist =

# Activités additionnelles
android.add_activites = 
android.meta_data = 
android.features = 

# Numéro de version (incrémenter à chaque build)
android.numeric_version = 1

# Signature (optionnelle pour debug)
android.signdebug = True

# Presplash
#presplash.filename = %(source.dir)s/data/presplash.png

# Icones multiples
#icon.xxxhdpi = %(source.dir)s/data/icon_xxxhdpi.png

# Configuration release (à remplir pour release)
# (android.keystore.*)

📱 Déploiement sur Android

flowchart TD A["💻 Code Prêt"] --> B["⚙️ Configuration Buildozer
buildozer.spec"] B --> C["🔨 Compilation APK
buildozer android debug"] C --> D["📦 APK Générée
bin/*.apk"] D --> E["📱 Transfert sur téléphone
ADB ou USB"] E --> F["🔧 Installation
Autoriser sources inconnues"] F --> G["⚙️ Configuration Android
• Paramètres > Apps > Appels
• Sélectionner comme app de filtrage"] G --> H["✅ Activation
• Lancer l'app
• Activer le bouclier"] H --> I["🎯 Test
Appeler depuis un autre téléphone"] style A fill:#11151f,stroke:#00ff88,stroke-width:2px style D fill:#11151f,stroke:#00d4ff,stroke-width:2px style I fill:#11151f,stroke:#00f5a0,stroke-width:2px

🚀 Compilation Pas à Pas

Copier
# 1. Initialiser Buildozer dans le dossier du projet
buildozer init

# 2. Éditer le fichier buildozer.spec avec la configuration fournie
# (Copier-coller le buildozer.spec du chapitre précédent)

# 3. Installer les dépendances système (Ubuntu/Debian)
sudo apt-get install -y \
    python3-pip \
    build-essential \
    git \
    python3-dev \
    ffmpeg \
    libsdl2-dev \
    libsdl2-image-dev \
    libsdl2-mixer-dev \
    libsdl2-ttf-dev \
    libportmidi-dev \
    libswscale-dev \
    libavformat-dev \
    libavcodec-dev \
    zlib1g-dev \
    libgstreamer1.0-0 \
    gstreamer1.0-plugins-base \
    gstreamer1.0-plugins-good

# 4. Compiler l'APK (première compilation peut prendre 30+ minutes)
buildozer -v android debug

# 5. Après compilation, l'APK se trouve dans :
# bin/bouclier_anti_spam-1.0-armeabi-v7a-debug.apk
# ou bin/bouclier_anti_spam-1.0-arm64-v8a-debug.apk

# 6. Transférer sur le téléphone
adb install bin/bouclier_anti_spam-1.0-armeabi-v7a-debug.apk
# Ou copier via USB et installer manuellement

# 7. Sur le téléphone :
# - Aller dans Paramètres > Sécurité > Sources inconnues > Autoriser
# - Installer l'APK
# - Après installation : Paramètres > Apps > Avancé > Accès aux apps > Appels
# - Sélectionner "Bouclier Anti-Spam" comme app de filtrage
# - Lancer l'app et activer le bouclier

# 8. Pour désinstaller :
adb uninstall org.pctamalou.antibouclierspam
# Ou via Paramètres > Apps > Désinstaller

⚠️ Problèmes Courants & Solutions :

  • Buildozer échoue : Vérifier Java JDK 11+ installé et JAVA_HOME défini
  • Permission denied : Sur Ubuntu, souvent besoin de sudo apt update avant
  • APK ne s'installe pas : Vérifier que "Sources inconnues" est activé
  • Service ne bloque pas : Vérifier dans Paramètres > Apps > Appels que l'app est sélectionnée
  • Notifications ne fonctionnent pas : Vérifier permissions de notification pour l'app
  • App crashes au lancement : Vérifier les logs avec adb logcat | grep python

📊 Patterns de Spam France 2025

Copier
# patterns.json - Base de données des spams français
{
  "last_updated": "2026-01-15",
  "version": "1.0",
  "patterns": {
    "telemarketing": [
      "0892", "0893", "0897", "0899",
      "0825", "0826", "0891", "0894",
      "0895", "0896", "0898"
    ],
    "surtaxes": [
      "0836", "0837", "0838", "0839",
      "0877", "0878", "0879"
    ],
    "spams_mobiles": [
      "0670", "0671", "0672", "0673", "0674", "0675",
      "0676", "0677", "0678", "0679",
      "0770", "0771", "0772", "0773", "0774", "0775"
    ],
    "nouveaux_2025": [
      "0946", "0947", "0948", "0949",
      "0977", "0978", "0979",
      "0980", "0981", "0982"
    ],
    "arnaque_fiscale": [
      "0170", "0171", "0172", "0173",
      "0180", "0181", "0182"
    ],
    "arnaque_banque": [
      "0153", "0154", "0155",
      "0162", "0163", "0164"
    ],
    "explications": {
      "0892-0899": "Numéros surtaxés télémarketing",
      "0836-0839": "Surtaxés divers",
      "0670-0679": "Spams mobiles récurrents",
      "0946-0949": "Nouveaux spams 2024-2026",
      "0977-0979": "Spams professionnels",
      "01xx": "Imitations services publics/banques"
    }
  },
  "metadata": {
    "source": "PCTAMALOU Community Reports",
    "country": "FR",
    "language": "fr",
    "update_frequency": "monthly"
  }
}

☢️ Option Nucléaire - Feature Secrète

🚨 OPTION NUCLÉAIRE ACTIVÉE

Cette fonctionnalité secrète est conçue pour les spammeurs extrêmement persistants qui changent constamment de numéro. Elle bloque non seulement le numéro exact, mais aussi toutes les variantes possibles autour de ce numéro.

flowchart LR A["📞 Spammeur Persistant
+33 1 23 45 67 89"] --> B["🔍 Analyse du Pattern"] B --> C["🎯 Extraction Base : +33 1 23 45 67"] C --> D["💥 Génération 1000 Variantes
+33 1 23 45 67 00 à 99"] D --> E["☢️ Blocage Massif
1000 numéros bloqués"] E --> F["📊 Résultat : Spammeur NEUTRALISÉ"] style A fill:#11151f,stroke:#ff006e,stroke-width:2px style E fill:#11151f,stroke:#ff6b00,stroke-width:3px style F fill:#11151f,stroke:#00ff88,stroke-width:2px
Copier
def nuclear_option(target_number: str) -> dict:
    """
    Option nucléaire - Bloque un spammeur et toutes ses variantes
    """
    # 1. Normalisation
    normalized = normalize_number(target_number)  # +33123456789
    
    if not normalized.startswith('+33'):
        return {"error": "Numéro français requis"}
    
    # 2. Extraction de la base (9 premiers chiffres après +33)
    base = normalized[3:12]  # "123456789" pour +33123456789
    
    if len(base) < 9:
        return {"error": "Numéro trop court"}
    
    # 3. Génération de toutes les variantes des 3 derniers chiffres
    blocked_variants = []
    
    for i in range(10):      # Premier chiffre variable
        for j in range(10):  # Deuxième chiffre variable
            for k in range(10):  # Troisième chiffre variable
                variant = base + str(i) + str(j) + str(k)
                full_number = "+33" + variant
                
                # Ajout à la liste noire
                add_to_blacklist(full_number)
                blocked_variants.append(full_number)
    
    # 4. Ajout du préfixe 6 chiffres aussi
    six_digit_prefix = base[:6]
    add_prefix_pattern(six_digit_prefix)
    
    # 5. Logs et notification
    log_nuclear_strike(target_number, len(blocked_variants))
    
    return {
        "status": "NUKE_SUCCESSFUL",
        "target": target_number,
        "base_pattern": base,
        "variants_blocked": len(blocked_variants),
        "message": f"☢️ NUKE ACTIVATED! {len(blocked_variants)} variants of {target_number} TERMINATED",
        "timestamp": datetime.now().isoformat()
    }

# Exemple d'utilisation :
result = nuclear_option("+33123456789")
print(result["message"])
# ☢️ NUKE ACTIVATED! 1000 variants of +33123456789 TERMINATED

⚠️ AVERTISSEMENT OPTION NUCLÉAIRE :

Cette option est extrême et peut bloquer des numéros légitimes si mal utilisée. À n'utiliser QUE pour :

  • Centres d'appels spam qui changent constamment les 3, 4 derniers chiffres
  • Spammeurs professionnels avec séquences numériques
  • Après avoir vérifié que c'est bien un spammeur persistant
  • Jamais sur des numéros personnels ou professionnels légitimes

⚖️ Éthique, Vie Privée & Licence

📜 Licence CC BY 4.0 - PCTAMALOU & Echoes of Hackers

Cette œuvre est mise à disposition selon les termes de la Licence CC BY 4.0, une licence de partage éthique :

  • ✅ Partage Libre : Vous pouvez copier, distribuer et communiquer le matériel
  • ✅ Adaptation Libre : Vous pouvez adapter et transformer l'œuvre
  • ✅ Usage Commercial Autorisé : Sous condition de don à une association
  • ✅ Attribution Requise : Vous devez créditer PCTAMALOU & Echoes of Hackers
  • ✅ Partage à l'identique : Si vous modifiez, partagez, c'est sous la même licence
  • ❌ Pas d'Usage Malveillant : Jamais pour harceler, spammer ou nuire

Contribution à l'association : Pour tout usage commercial, 10% des bénéfices doivent être reversés à une association caritative (liste sur HelloAsso).

graph TD A["📱 Application"] --> B{🔒 Données} B --> C["📊 Patterns Spam
Stockage local JSON"] B --> D["📈 Statistiques
Compteurs locaux"] B --> E["🚫 Aucune Donnée Personnelle
Pas de contacts, SMS, etc."] F["🌐 Connexions"] --> G["📡 Aucune Connexion Sortante
Pas de serveurs, pas de tracking"] F --> H["🔐 Tout est Local
Même les patterns sont en dur"] F --> I["📴 Mode Avion Compatible
Fonctionne sans Internet"] J["👁️ Transparence"] --> K["📖 Code Open Source
Inspection complète possible"] J --> L["🔍 Aucune Permission Superflue
Uniquement celles nécessaires"] J --> M["🗑️ Effacement Facile
Désinstaller = tout supprimer"] style C fill:#11151f,stroke:#00ff88,stroke-width:2px style G fill:#11151f,stroke:#00d4ff,stroke-width:2px style K fill:#11151f,stroke:#9d4edd,stroke-width:2px

🛡️ Charte Éthique du Développeur

En tant que développeur/utilisateur du Bouclier Anti-Spam, je m'engage à :

  • Respecter la vie privée : Ne jamais collecter de données personnelles
  • Être transparent : Expliquer clairement ce que fait l'application
  • Limiter les permissions : Ne demander que le strict nécessaire
  • Protéger les utilisateurs : Bloquer uniquement les vrais spams
  • Partager les connaissances : Contribuer à améliorer les patterns pour tous
  • Respecter la loi : Se conformer au RGPD et lois françaises

📝 Déclaration RGPD

Conformité RGPD totale : Cette application ne collecte, ne traite, ni ne transmet aucune donnée personnelle.

  • Responsable du traitement : L'utilisateur lui-même sur son appareil
  • Données traitées : Numéros de téléphone entrants (traitement temporaire)
  • Stockage : Local uniquement, sur l'appareil de l'utilisateur
  • Durée de conservation : Jusqu'à désinstallation de l'application
  • Droits des personnes : Droit d'accès, rectification, effacement via désinstallation
  • Sous-traitants : Aucun, tout est exécuté localement

L'utilisateur reste propriétaire et responsable de ses données à tout moment.

🤝 Communauté & Contributions

flowchart TD A["👥 Communauté PCTAMALOU"] --> B["📊 Base de Patterns Collaborative"] A --> C["🐛 Reporting de Bugs"] A --> D["💡 Nouvelles Fonctionnalités"] A --> E["🌍 Traductions"] B --> F["🔄 Mises à jour Mensuelles
Nouveaux patterns de spam"] C --> G["🔧 Corrections Rapides
Support communautaire"] D --> H["🚀 Évolution du Projet
Features votées"] E --> I["🌐 Internationalisation
Support multi-langues"] J["📈 Impact"] --> K["🇫🇷 France : 1000+ utilisateurs"] J --> L["🇪🇺 Europe : Adaptation en cours"] J --> M["🌍 Monde : Version internationale prévue"] style A fill:#11151f,stroke:#9d4edd,stroke-width:3px style B fill:#11151f,stroke:#00ff88,stroke-width:2px style K fill:#11151f,stroke:#00d4ff,stroke-width:2px

🔧 Comment Contribuer

🐛 Reporter un Bug

Contact : admin@pctamalou.fr.

💡 Suggérer une Feature

Contact : admin@pctamalou.fr.

🔍 Ajouter des Patterns

Contribuer à la base de patterns : nouveaux préfixes spam, numéros connus. Contact : admin@pctamalou.fr pour être cité et publié ici.

🌍 Traduire

Traduction de l'interface et documentation dans d'autres langues. Contact : admin@pctamalou.fr pour être cité et publié ici.