Initial commit

This commit is contained in:
Joey
2026-08-06 12:38:23 +00:00
commit 23348e8349
8 changed files with 719 additions and 0 deletions
+47
View File
@@ -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
+288
View File
@@ -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
+12
View File
@@ -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>
+9
View File
@@ -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"
}
+3
View File
@@ -0,0 +1,3 @@
SIGNAL_NUMBER=+44..
SIGNAL_HOST=localhost
SIGNAL_PORT=8080
+3
View File
@@ -0,0 +1,3 @@
pysignalclijsonrpc>=25.9.0
sseclient-py>=1.8.0
requests>=2.31.0
Executable
+8
View File
@@ -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
+349
View File
@@ -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()