Application Android ultime pour bloquer définitivement les spammeurs téléphoniques en France. Filtrage intelligent basé sur patterns, 100% local, zéro tracking.
"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
# 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+
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
#!/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()
#!/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()
[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.*)
# 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
adb logcat | grep python# 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"
}
}
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.
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
Cette option est extrême et peut bloquer des numéros légitimes si mal utilisée. À n'utiliser QUE pour :
Cette œuvre est mise à disposition selon les termes de la Licence CC BY 4.0, une licence de partage éthique :
Contribution à l'association : Pour tout usage commercial, 10% des bénéfices doivent être reversés à une association caritative (liste sur HelloAsso).
Conformité RGPD totale : Cette application ne collecte, ne traite, ni ne transmet aucune donnée personnelle.
L'utilisateur reste propriétaire et responsable de ses données à tout moment.
Contact : admin@pctamalou.fr.
Contact : admin@pctamalou.fr.
Contribuer à la base de patterns : nouveaux préfixes spam, numéros connus. Contact : admin@pctamalou.fr pour être cité et publié ici.
Traduction de l'interface et documentation dans d'autres langues. Contact : admin@pctamalou.fr pour être cité et publié ici.