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