Initial commit
This commit is contained in:
+47
@@ -0,0 +1,47 @@
|
|||||||
|
FROM debian:stable-slim
|
||||||
|
ARG SIGNAL_CLI_VER=0.14.7
|
||||||
|
ARG TEMURIN_JDK_URL=https://github.com/adoptium/temurin25-binaries/releases/download/jdk-25.0.4%2B7/OpenJDK25U-jdk_x64_linux_hotspot_25.0.4_7.tar.gz
|
||||||
|
ARG TEMURIN_JDK_SHA256=e58fcdcd637b25c03ca84cbbcefc70d11efb8f4b4cbd05decc9f661769d77f94
|
||||||
|
ARG JDK_MAJOR=25
|
||||||
|
ENV JAVA_HOME=/usr/lib/jvm/java-${JDK_MAJOR}
|
||||||
|
|
||||||
|
RUN apt update && apt -y upgrade
|
||||||
|
RUN apt install -y wget ca-certificates
|
||||||
|
RUN rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
RUN wget -qO /tmp/jdk.tar.gz "$TEMURIN_JDK_URL"
|
||||||
|
RUN echo "${TEMURIN_JDK_SHA256} /tmp/jdk.tar.gz" | sha256sum -c -
|
||||||
|
RUN mkdir /tmp/temurin && tar xvzf /tmp/jdk.tar.gz -C /tmp/temurin
|
||||||
|
RUN mkdir -p "$JAVA_HOME" && mv /tmp/temurin/*/* "$JAVA_HOME"/ && rm -f /tmp/jdk.tar.gz
|
||||||
|
|
||||||
|
RUN update-alternatives --install /usr/bin/java java ${JAVA_HOME}/bin/java ${JDK_MAJOR}0
|
||||||
|
RUN update-alternatives --set java /usr/lib/jvm/java-${JDK_MAJOR}/bin/java
|
||||||
|
|
||||||
|
|
||||||
|
WORKDIR /opt
|
||||||
|
RUN wget https://github.com/AsamK/signal-cli/releases/download/v${SIGNAL_CLI_VER}/signal-cli-${SIGNAL_CLI_VER}.tar.gz
|
||||||
|
RUN tar xvfz signal-cli-${SIGNAL_CLI_VER}.tar.gz
|
||||||
|
RUN ln -s /opt/signal-cli-${SIGNAL_CLI_VER} /opt/signal-cli
|
||||||
|
|
||||||
|
RUN mkdir /app \
|
||||||
|
&& useradd app -u 6789
|
||||||
|
|
||||||
|
|
||||||
|
COPY requirements.txt /app
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN apt update && apt install -y python3-pip
|
||||||
|
RUN pip install --break-system-packages --no-cache-dir -r /app/requirements.txt
|
||||||
|
|
||||||
|
RUN apt update && apt-get install -y --no-install-recommends libmagic1 \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
COPY . /app
|
||||||
|
RUN chown -R app /app
|
||||||
|
USER app
|
||||||
|
|
||||||
|
|
||||||
|
#ENTRYPOINT /opt/signal-cli/bin/signal-cli --config=/app/signal-cli-data
|
||||||
|
ENTRYPOINT /app/spam_remover.py
|
||||||
|
CMD /app/run.sh
|
||||||
@@ -0,0 +1,288 @@
|
|||||||
|
# Signal Spammer Auto-Removal
|
||||||
|
|
||||||
|
Automatically removes spammers from your Signal group chats when they message in a designated honeypot group.
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
1. You have a "honeypot" group that's clearly not for humans (e.g., "AI Bot Testing" or "System Messages")
|
||||||
|
2. Spammers join all your groups and message in every chat
|
||||||
|
3. When a spammer messages in the honeypot group, this script detects them
|
||||||
|
4. The script removes them from **all** groups you admin (except the honeypot)
|
||||||
|
5. You get a notification about the removal
|
||||||
|
|
||||||
|
The spammer never realizes they've been caught - they still see the honeypot group and think everything is working.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- **Java 25+** (required by signal-cli 0.14.x)
|
||||||
|
- **signal-cli 0.14.2+** (has the group member removal fix)
|
||||||
|
- **Python 3.9+**
|
||||||
|
- A Signal account registered with signal-cli
|
||||||
|
|
||||||
|
## Step 1: Install Java
|
||||||
|
|
||||||
|
signal-cli requires Java 25 or newer. Install OpenJDK:
|
||||||
|
|
||||||
|
**macOS (Homebrew):**
|
||||||
|
```bash
|
||||||
|
brew install openjdk@21
|
||||||
|
# or for Java 25+:
|
||||||
|
brew install openjdk
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ubuntu/Debian:**
|
||||||
|
```bash
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install openjdk-21-jdk
|
||||||
|
```
|
||||||
|
|
||||||
|
**Verify:**
|
||||||
|
```bash
|
||||||
|
java --version
|
||||||
|
# Should show 21+
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 2: Install signal-cli
|
||||||
|
|
||||||
|
**Option A: Download release (recommended)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Download latest release (check https://github.com/AsamK/signal-cli/releases for current version)
|
||||||
|
cd /tmp
|
||||||
|
wget https://github.com/AsamK/signal-cli/releases/download/v0.14.5/signal-cli-0.14.5-Linux.tar.gz
|
||||||
|
|
||||||
|
# Extract
|
||||||
|
tar xf signal-cli-0.14.5-Linux.tar.gz
|
||||||
|
|
||||||
|
# Move to /opt
|
||||||
|
sudo mv signal-cli-0.14.5 /opt/signal-cli
|
||||||
|
|
||||||
|
# Add to PATH
|
||||||
|
echo 'export PATH="/opt/signal-cli:$PATH"' >> ~/.bashrc
|
||||||
|
source ~/.bashrc
|
||||||
|
```
|
||||||
|
|
||||||
|
**Option B: macOS with Homebrew**
|
||||||
|
```bash
|
||||||
|
brew install signal-cli
|
||||||
|
```
|
||||||
|
|
||||||
|
**Option C: Build from source**
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/AsamK/signal-cli.git
|
||||||
|
cd signal-cli
|
||||||
|
./gradlew installDist
|
||||||
|
# Binary will be in build/install/signal-cli/bin/signal-cli
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 3: Register signal-cli with Your Signal Account
|
||||||
|
|
||||||
|
**Important:** You need a phone number that can receive SMS or calls. This can be:
|
||||||
|
- Your main number (if you want to use your existing account)
|
||||||
|
- A secondary number (dedicated for this bot)
|
||||||
|
|
||||||
|
**Register:**
|
||||||
|
```bash
|
||||||
|
# Using your phone number
|
||||||
|
signal-cli -a +1234567890 register
|
||||||
|
|
||||||
|
# You'll receive an SMS with a verification code
|
||||||
|
signal-cli -a +1234567890 verify CODE_FROM_SMS
|
||||||
|
```
|
||||||
|
|
||||||
|
**Alternative - Link as secondary device:**
|
||||||
|
If you want to use your existing Signal account as a linked device:
|
||||||
|
```bash
|
||||||
|
signal-cli link
|
||||||
|
# Shows a URI - scan this QR code with your phone's Signal app
|
||||||
|
# Go to Signal Settings > Linked Devices > Link New Device
|
||||||
|
```
|
||||||
|
|
||||||
|
**Set a PIN (recommended):**
|
||||||
|
```bash
|
||||||
|
signal-cli -a +1234567890 setPin YOUR_PIN
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 4: Start signal-cli Daemon
|
||||||
|
|
||||||
|
The script communicates with signal-cli via its JSON-RPC HTTP interface.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start daemon on localhost:8080
|
||||||
|
signal-cli -a +1234567890 daemon --http=localhost:8080
|
||||||
|
```
|
||||||
|
|
||||||
|
**Keep this running.** You may want to run it as a systemd service:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create systemd service
|
||||||
|
sudo tee /etc/systemd/system/signal-cli.service << 'EOF'
|
||||||
|
[Unit]
|
||||||
|
Description=Signal CLI Daemon
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=YOUR_USERNAME
|
||||||
|
ExecStart=/opt/signal-cli/bin/signal-cli -a +1234567890 daemon --http=localhost:8080
|
||||||
|
Restart=always
|
||||||
|
RestartSec=10
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Enable and start
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl enable signal-cli
|
||||||
|
sudo systemctl start signal-cli
|
||||||
|
|
||||||
|
# Check status
|
||||||
|
sudo systemctl status signal-cli
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 5: Get Your Honeypot Group ID
|
||||||
|
|
||||||
|
1. Create the honeypot group in Signal (app or signal-cli)
|
||||||
|
2. Get the group's base64 ID:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
signal-cli -a +1234567890 listGroups --output=json | jq '.[] | select(.name=="YOUR_HONEYPOT_GROUP_NAME") | .id'
|
||||||
|
```
|
||||||
|
|
||||||
|
Or list all groups:
|
||||||
|
```bash
|
||||||
|
signal-cli -a +1234567890 listGroups --output=json | jq '.[] | {name, id}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 6: Install Python Dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /path/to/this/project
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 7: Configure
|
||||||
|
|
||||||
|
Edit `config.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"signal_cli_endpoint": "http://localhost:8080",
|
||||||
|
"account": "+1234567890",
|
||||||
|
"honeypot_group_id": "BASE64_GROUP_ID_FROM_STEP_5",
|
||||||
|
"notify_self_number": "+1234567890",
|
||||||
|
"excluded_groups": [],
|
||||||
|
"banned_list_path": "banned.json",
|
||||||
|
"log_file": "spam_remover.log"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Configuration options:**
|
||||||
|
|
||||||
|
| Field | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `signal_cli_endpoint` | URL where signal-cli daemon is running |
|
||||||
|
| `account` | Your signal-cli registered phone number |
|
||||||
|
| `honeypot_group_id` | Base64 ID of the honeypot group |
|
||||||
|
| `notify_self_number` | Phone number to receive removal notifications |
|
||||||
|
| `excluded_groups` | List of group IDs to never remove from (besides honeypot) |
|
||||||
|
| `banned_list_path` | Path to store banned spammers list |
|
||||||
|
| `log_file` | Path to log file |
|
||||||
|
|
||||||
|
## Step 8: Run
|
||||||
|
|
||||||
|
**Daemon mode (recommended):**
|
||||||
|
```bash
|
||||||
|
python spam_remover.py
|
||||||
|
```
|
||||||
|
|
||||||
|
This runs continuously, listening for messages in the honeypot group.
|
||||||
|
|
||||||
|
**Manual ban:**
|
||||||
|
```bash
|
||||||
|
python spam_remover.py --ban +1987654321
|
||||||
|
```
|
||||||
|
|
||||||
|
**Dry run (preview):**
|
||||||
|
```bash
|
||||||
|
python spam_remover.py --dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
**List banned:**
|
||||||
|
```bash
|
||||||
|
python spam_remover.py --list-banned
|
||||||
|
```
|
||||||
|
|
||||||
|
**Single check:**
|
||||||
|
```bash
|
||||||
|
python spam_remover.py --once
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running as a Service
|
||||||
|
|
||||||
|
Create a systemd service for the spam remover:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo tee /etc/systemd/system/spam-remover.service << 'EOF'
|
||||||
|
[Unit]
|
||||||
|
Description=Signal Spammer Auto-Remover
|
||||||
|
After=network.target signal-cli.service
|
||||||
|
Requires=signal-cli.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=YOUR_USERNAME
|
||||||
|
WorkingDirectory=/path/to/this/project
|
||||||
|
ExecStart=/usr/bin/python3 spam_remover.py
|
||||||
|
Restart=always
|
||||||
|
RestartSec=30
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl enable spam-remover
|
||||||
|
sudo systemctl start spam-remover
|
||||||
|
```
|
||||||
|
|
||||||
|
## Files Created
|
||||||
|
|
||||||
|
| File | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `spam_remover.py` | Main script |
|
||||||
|
| `config.json` | Configuration |
|
||||||
|
| `requirements.txt` | Python dependencies |
|
||||||
|
| `banned.json` | Auto-created, stores banned spammers |
|
||||||
|
| `spam_remover.log` | Auto-created, application logs |
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
**signal-cli not connecting:**
|
||||||
|
```bash
|
||||||
|
# Check if daemon is running
|
||||||
|
curl http://localhost:8080/api/v1/check
|
||||||
|
|
||||||
|
# Check signal-cli status
|
||||||
|
systemctl status signal-cli
|
||||||
|
```
|
||||||
|
|
||||||
|
**Permission errors:**
|
||||||
|
- Ensure your account is admin of the groups you want to manage
|
||||||
|
- signal-cli can only remove members from groups where you have admin privileges
|
||||||
|
|
||||||
|
**Rate limiting:**
|
||||||
|
- Signal may rate-limit if you remove too many people too fast
|
||||||
|
- The script handles this automatically with retries
|
||||||
|
|
||||||
|
**Groups not showing:**
|
||||||
|
- Run `signal-cli -a +1234567890 listGroups` to verify groups are synced
|
||||||
|
- You may need to link your device first to sync existing groups
|
||||||
|
|
||||||
|
## Security Notes
|
||||||
|
|
||||||
|
- The honeypot group should look legitimate enough that spammers join but obvious enough that real users don't
|
||||||
|
- The `banned.json` file contains phone numbers - protect this file
|
||||||
|
- signal-cli registration requires a real phone number for SMS verification
|
||||||
|
- Running as a linked device means your primary device must stay online
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
## How to use
|
||||||
|
|
||||||
|
# 1. Initial config
|
||||||
|
# Signal config:
|
||||||
|
docker run -it -v $PWD/signal-cli-data:/app/signal-cli-data <image> /opt/signal-cli/bin/signal-cli
|
||||||
|
# spam_remover.py config:
|
||||||
|
docker run -it -v $PWD/signal-cli-data:/app/signal-cli-data <image> /app/spam_remover.py
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# 2. Run when configured
|
||||||
|
docker run -v $PWD/config.json:/app/config.json -v $PWD/env:/app/env -v $PWD/signal-cli-data:/app/signal-cli-data -d <image>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"signal_cli_endpoint": "http://localhost:8080",
|
||||||
|
"account": "+YOUR_PHONE_NUMBER",
|
||||||
|
"honeypot_group_id": "PASTE_BASE64_GROUP_ID_HERE",
|
||||||
|
"notify_self_number": "+YOUR_PHONE_NUMBER",
|
||||||
|
"excluded_groups": [],
|
||||||
|
"banned_list_path": "banned.json",
|
||||||
|
"log_file": "spam_remover.log"
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
pysignalclijsonrpc>=25.9.0
|
||||||
|
sseclient-py>=1.8.0
|
||||||
|
requests>=2.31.0
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
. /app/env
|
||||||
|
|
||||||
|
|
||||||
|
/opt/signal-cli/bin/signal-cli -a $SIGNAL_NUMBER daemon --http=$SIGNAL_HOST:$SIGNAL_PORT --config=/app/signal-cli-data &
|
||||||
|
|
||||||
|
/app/spam_remover.py
|
||||||
Executable
+349
@@ -0,0 +1,349 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Signal Spammer Auto-Removal System
|
||||||
|
|
||||||
|
Monitors a honeypot Signal group chat, detects spammers who message there,
|
||||||
|
and automatically removes them from all groups you admin.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python spam_remover.py # Daemon mode (SSE listener)
|
||||||
|
python spam_remover.py --ban NUM # Manually ban a number
|
||||||
|
python spam_remover.py --dry-run # Preview without changes
|
||||||
|
python spam_remover.py --once # Single check and exit
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import sseclient
|
||||||
|
from pysignalclijsonrpc import SignalCliJSONRPCApi
|
||||||
|
|
||||||
|
|
||||||
|
class SpammerRemover:
|
||||||
|
def __init__(self, config_path: str = "config.json"):
|
||||||
|
self.config = self._load_config(config_path)
|
||||||
|
self.banned_list_path = Path(self.config["banned_list_path"])
|
||||||
|
self.banned_list = self._load_banned_list()
|
||||||
|
self._setup_logging()
|
||||||
|
self.api = SignalCliJSONRPCApi(
|
||||||
|
endpoint=self.config["signal_cli_endpoint"],
|
||||||
|
account=self.config["account"],
|
||||||
|
)
|
||||||
|
self.logger.info(
|
||||||
|
f"Initialized. Honeypot: {self.config['honeypot_group_id'][:12]}..."
|
||||||
|
)
|
||||||
|
|
||||||
|
def _load_config(self, config_path: str) -> dict[str, Any]:
|
||||||
|
path = Path(config_path)
|
||||||
|
if not path.exists():
|
||||||
|
print(f"Error: Config file not found: {config_path}")
|
||||||
|
print("Create config.json from the template first.")
|
||||||
|
sys.exit(1)
|
||||||
|
with open(path) as f:
|
||||||
|
return json.load(f)
|
||||||
|
|
||||||
|
def _setup_logging(self):
|
||||||
|
log_file = self.config.get("log_file", "spam_remover.log")
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format="%(asctime)s [%(levelname)s] %(message)s",
|
||||||
|
handlers=[
|
||||||
|
logging.FileHandler(log_file),
|
||||||
|
logging.StreamHandler(sys.stdout),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
self.logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
def _load_banned_list(self) -> dict[str, Any]:
|
||||||
|
if self.banned_list_path.exists():
|
||||||
|
with open(self.banned_list_path) as f:
|
||||||
|
return json.load(f)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def _save_banned_list(self):
|
||||||
|
with open(self.banned_list_path, "w") as f:
|
||||||
|
json.dump(self.banned_list, f, indent=2)
|
||||||
|
|
||||||
|
def is_banned(self, number: str) -> bool:
|
||||||
|
return number in self.banned_list
|
||||||
|
|
||||||
|
def ban_number(self, number: str, reason: str = "honeypot_message"):
|
||||||
|
if self.is_banned(number):
|
||||||
|
self.logger.info(f"Already banned: {number}")
|
||||||
|
return
|
||||||
|
|
||||||
|
self.banned_list[number] = {
|
||||||
|
"banned_at": datetime.now(timezone.utc).isoformat(),
|
||||||
|
"reason": reason,
|
||||||
|
"removed_from": [],
|
||||||
|
}
|
||||||
|
self._save_banned_list()
|
||||||
|
self.logger.info(f"Banned: {number}")
|
||||||
|
|
||||||
|
def get_groups(self) -> list[dict[str, Any]]:
|
||||||
|
try:
|
||||||
|
return self.api.list_groups()
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Failed to list groups: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
def get_admin_groups(self) -> list[dict[str, Any]]:
|
||||||
|
groups = self.get_groups()
|
||||||
|
admin_groups = []
|
||||||
|
for group in groups:
|
||||||
|
if not group.get("isMember", False):
|
||||||
|
continue
|
||||||
|
admins = group.get("admins", [])
|
||||||
|
members = group.get("members", [])
|
||||||
|
account = self.config["account"]
|
||||||
|
if account in admins or account in members:
|
||||||
|
if account in admins:
|
||||||
|
admin_groups.append(group)
|
||||||
|
return admin_groups
|
||||||
|
|
||||||
|
def remove_from_group(self, group_id: str, group_name: str, number: str) -> bool:
|
||||||
|
try:
|
||||||
|
group = self.api.get_group(group_id)
|
||||||
|
members = group.get("members", [])
|
||||||
|
|
||||||
|
member_numbers = []
|
||||||
|
for m in members:
|
||||||
|
if isinstance(m, dict):
|
||||||
|
member_numbers.append(m.get("number", m.get("uuid", "")))
|
||||||
|
else:
|
||||||
|
member_numbers.append(str(m))
|
||||||
|
|
||||||
|
if number not in member_numbers:
|
||||||
|
self.logger.info(f"{number} not in group {group_name}, skipping")
|
||||||
|
return True
|
||||||
|
|
||||||
|
updated_members = [m for m in member_numbers if m != number]
|
||||||
|
|
||||||
|
self.api.update_group(
|
||||||
|
name=group.get("name", ""),
|
||||||
|
members=updated_members,
|
||||||
|
admins=group.get("admins", []),
|
||||||
|
description=group.get("description", ""),
|
||||||
|
group_link=group.get("groupInviteLink", "disabled"),
|
||||||
|
)
|
||||||
|
self.logger.info(f"Removed {number} from {group_name}")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Failed to remove from {group_name}: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def send_notification(self, message: str):
|
||||||
|
try:
|
||||||
|
self.api.send_message(
|
||||||
|
recipients=[self.config["notify_self_number"]],
|
||||||
|
message=message,
|
||||||
|
)
|
||||||
|
self.logger.info(f"Notification sent: {message[:50]}...")
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Failed to send notification: {e}")
|
||||||
|
|
||||||
|
def process_spammer(self, number: str, dry_run: bool = False) -> dict[str, Any]:
|
||||||
|
result = {
|
||||||
|
"number": number,
|
||||||
|
"already_banned": self.is_banned(number),
|
||||||
|
"groups_removed": [],
|
||||||
|
"groups_failed": [],
|
||||||
|
"dry_run": dry_run,
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.is_banned(number):
|
||||||
|
self.logger.info(f"Already banned: {number}")
|
||||||
|
return result
|
||||||
|
|
||||||
|
if dry_run:
|
||||||
|
self.logger.info(f"[DRY RUN] Would ban: {number}")
|
||||||
|
admin_groups = self.get_admin_groups()
|
||||||
|
for group in admin_groups:
|
||||||
|
gid = group.get("id", "")
|
||||||
|
gname = group.get("name", "Unknown")
|
||||||
|
if gid == self.config["honeypot_group_id"]:
|
||||||
|
continue
|
||||||
|
if gid in self.config.get("excluded_groups", []):
|
||||||
|
continue
|
||||||
|
result["groups_removed"].append(gname)
|
||||||
|
return result
|
||||||
|
|
||||||
|
self.ban_number(number)
|
||||||
|
|
||||||
|
admin_groups = self.get_admin_groups()
|
||||||
|
removed_count = 0
|
||||||
|
|
||||||
|
for group in admin_groups:
|
||||||
|
gid = group.get("id", "")
|
||||||
|
gname = group.get("name", "Unknown")
|
||||||
|
|
||||||
|
if gid == self.config["honeypot_group_id"]:
|
||||||
|
continue
|
||||||
|
if gid in self.config.get("excluded_groups", []):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if self.remove_from_group(gid, gname, number):
|
||||||
|
result["groups_removed"].append(gname)
|
||||||
|
removed_count += 1
|
||||||
|
else:
|
||||||
|
result["groups_failed"].append(gname)
|
||||||
|
|
||||||
|
if removed_count > 0:
|
||||||
|
self.banned_list[number]["removed_from"] = result["groups_removed"]
|
||||||
|
self._save_banned_list()
|
||||||
|
|
||||||
|
notification = (
|
||||||
|
f"Spammer removed: {number}\n"
|
||||||
|
f"Removed from {removed_count} group(s): {', '.join(result['groups_removed'])}"
|
||||||
|
)
|
||||||
|
self.send_notification(notification)
|
||||||
|
|
||||||
|
if result["groups_failed"]:
|
||||||
|
self.logger.warning(
|
||||||
|
f"Failed to remove from {len(result['groups_failed'])} groups"
|
||||||
|
)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def handle_message(self, envelope: dict):
|
||||||
|
source = envelope.get("sourceNumber") or envelope.get("source", "")
|
||||||
|
if not source:
|
||||||
|
return
|
||||||
|
|
||||||
|
group_info = envelope.get("groupInfo", {})
|
||||||
|
group_id = group_info.get("groupId", "")
|
||||||
|
|
||||||
|
if group_id != self.config["honeypot_group_id"]:
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.is_banned(source):
|
||||||
|
return
|
||||||
|
|
||||||
|
self.logger.info(f"Honeypot message from: {source}")
|
||||||
|
result = self.process_spammer(source)
|
||||||
|
self.logger.info(
|
||||||
|
f"Processed {source}: removed from {len(result['groups_removed'])} groups"
|
||||||
|
)
|
||||||
|
|
||||||
|
def daemon_mode(self):
|
||||||
|
self.logger.info("Starting daemon mode (SSE listener)...")
|
||||||
|
endpoint = self.config["signal_cli_endpoint"]
|
||||||
|
url = f"{endpoint}/api/v1/events"
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
self.logger.info(f"Connecting to SSE: {url}")
|
||||||
|
response = requests.get(url, stream=True, timeout=60)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
client = sseclient.SSEClient(response)
|
||||||
|
for event in client.events():
|
||||||
|
if event.event == "message":
|
||||||
|
try:
|
||||||
|
data = json.loads(event.data)
|
||||||
|
envelope = data.get("envelope", {})
|
||||||
|
self.handle_message(envelope)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
self.logger.warning(f"Invalid JSON: {event.data[:100]}")
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Error processing event: {e}")
|
||||||
|
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
self.logger.warning("Connection lost. Reconnecting in 5s...")
|
||||||
|
time.sleep(5)
|
||||||
|
except requests.exceptions.Timeout:
|
||||||
|
self.logger.debug("SSE timeout, reconnecting...")
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"SSE error: {e}. Reconnecting in 5s...")
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
def once_mode(self):
|
||||||
|
self.logger.info("Running single check...")
|
||||||
|
admin_groups = self.get_admin_groups()
|
||||||
|
self.logger.info(f"Found {len(admin_groups)} admin'd groups")
|
||||||
|
|
||||||
|
for group in admin_groups:
|
||||||
|
gid = group.get("id", "")
|
||||||
|
gname = group.get("name", "Unknown")
|
||||||
|
members = group.get("members", [])
|
||||||
|
|
||||||
|
member_count = len(members) if isinstance(members, list) else 0
|
||||||
|
self.logger.info(f" {gname}: {member_count} members")
|
||||||
|
|
||||||
|
def manual_mode(self, number: str):
|
||||||
|
self.logger.info(f"Manual ban: {number}")
|
||||||
|
result = self.process_spammer(number)
|
||||||
|
|
||||||
|
if result["already_banned"]:
|
||||||
|
print(f"Already banned: {number}")
|
||||||
|
else:
|
||||||
|
print(f"Banned: {number}")
|
||||||
|
if result["groups_removed"]:
|
||||||
|
print(f"Removed from: {', '.join(result['groups_removed'])}")
|
||||||
|
if result["groups_failed"]:
|
||||||
|
print(f"Failed to remove from: {', '.join(result['groups_failed'])}")
|
||||||
|
|
||||||
|
def list_banned(self):
|
||||||
|
if not self.banned_list:
|
||||||
|
print("No banned numbers.")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"Banned numbers ({len(self.banned_list)}):")
|
||||||
|
for number, info in self.banned_list.items():
|
||||||
|
banned_at = info.get("banned_at", "unknown")
|
||||||
|
removed = info.get("removed_from", [])
|
||||||
|
print(f" {number} (banned {banned_at}) - removed from {len(removed)} groups")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Signal Spammer Auto-Removal System"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--config",
|
||||||
|
default="config.json",
|
||||||
|
help="Path to config file (default: config.json)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--ban",
|
||||||
|
metavar="NUMBER",
|
||||||
|
help="Manually ban a phone number",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--dry-run",
|
||||||
|
action="store_true",
|
||||||
|
help="Preview what would be done without making changes",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--once",
|
||||||
|
action="store_true",
|
||||||
|
help="Single check and exit",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--list-banned",
|
||||||
|
action="store_true",
|
||||||
|
help="List all banned numbers",
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
remover = SpammerRemover(config_path=args.config)
|
||||||
|
|
||||||
|
if args.list_banned:
|
||||||
|
remover.list_banned()
|
||||||
|
elif args.ban:
|
||||||
|
remover.manual_mode(args.ban)
|
||||||
|
elif args.once:
|
||||||
|
remover.once_mode()
|
||||||
|
else:
|
||||||
|
remover.daemon_mode()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user