# Telegram filter bot and Bluesky poster

> Source: <https://github.com/bananaosint/bsky-poster>
> Published: 2026-06-14 08:35:04+00:00

A three-bot system that monitors Telegram OSINT/geopolitics channels, filters messages through a keyword pre-filter and LLM evaluation, and automatically posts approved stories to Bluesky with AI-generated summaries, image analysis, and automatic story threading.

```
┌─────────────────────────────────────────────────────────┐
│                     controlbot.py                       │
│          Telegram bot that manages the other two        │
│     /start, /stop, /restart, /startall, /logs, etc.     │
└──────────────┬──────────────────────┬───────────────────┘
               │ manages              │ manages
               ▼                      ▼
┌──────────────────────────┐  ┌──────────────────────────┐
│      filterbot.py        │  │      posterbot.py         │
│                          │  │                           │
│ Monitors TG channels     │  │ Watches filterbot output  │
│ Keyword pre-filter       │──│ AI-formats for Bluesky    │
│ Semantic deduplication   │  │ Vision: logo detection    │
│ Auto-translation         │  │ Vision: auto-captioning   │
│ LLM evaluation (Groq)    │  │ Semantic deduplication    │
│ Urgency tagging          │  │ Story threading           │
│ Forwards to destination  │  │ Posts to Bluesky          │
└──────────────────────────┘  └──────────────────────────┘
```

| File | Description |
|---|---|
`controlbot.py` |
Telegram bot supervisor. Runs as the parent process and manages filterbot and posterbot as child processes. Send commands from your phone to start/stop/restart bots, view logs, and check heartbeat stats. |
`filterbot.py` |
The core intelligence filter. Connects to your Telegram user account via Telethon, monitors source channels, runs a three-tier keyword pre-filter (override → instant-reject → must-match), detects semantic duplicates, auto-translates non-English messages, and evaluates importance via Groq LLM. Approved messages are forwarded to a destination Telegram channel with urgency tags (🔴 FLASH / 🟡 NOTABLE). |
`posterbot.py` |
Bluesky publisher. Watches the destination channel that filterbot writes to, batches incoming messages, uses Groq to rewrite them as concise Bluesky posts (≤300 chars), runs vision analysis on images (logo detection + auto-captioning), detects story updates and posts them as threaded replies, and publishes to Bluesky. |
`(TOOL)export_telegram_channels.py` |
Utility script. Connects to your Telegram account and exports all channels you're subscribed to into a `channels.json` config file. This is the first thing you run when setting up the project. |
`channels.json` |
Your personal channel configuration (gitignored). Contains channel IDs, names, credibility tiers, and the destination channel. Generated by the export tool. |
`channels.example.json` |
Template showing the `channels.json` format. Copy this and fill it in if you prefer manual setup over the export tool. |
`filters.json` |
Your keyword filters and LLM prompt (gitignored). Defines what topics the bot looks for, what it rejects, and how the AI evaluates messages. |
`filters.example.json` |
Template showing the `filters.json` format with placeholder keywords. Copy and customize for your use case. |
`private.env` |
Environment variables file containing all API keys and tokens. Never commit this to Git. |

You need credentials from **four** services:

| Service | What you need | Where to get it |
|---|---|---|
Telegram API |
`TELEGRAM_API_ID` and `TELEGRAM_API_HASH` |
|

**Telegram Bot**`CONTROL_BOT_TOKEN`

[@BotFather](https://t.me/BotFather)on Telegram →`/newbot`

**Telegram User ID**`ADMIN_TELEGRAM_ID`

[@userinfobot](https://t.me/userinfobot)on Telegram**Groq**`GROQ_API_KEY`

[console.groq.com/keys](https://console.groq.com/keys)— free tier available**Bluesky**`BLUESKY_HANDLE`

and `BLUESKY_APP_PASSWORD`

[bsky.app/settings/app-passwords](https://bsky.app/settings/app-passwords)| Software | Download |
|---|---|
Python 3.11+ |
|

```
pip install telethon python-telegram-bot python-dotenv deep-translator groq pydantic langdetect sentence-transformers torch atproto
git clone https://github.com/bananaosint/bsky-poster.git
cd bsky-poster
pip install telethon python-telegram-bot python-dotenv deep-translator groq pydantic langdetect sentence-transformers torch atproto
```

Create a file called `private.env`

in the project root with the following contents:

```
# Telegram
TELEGRAM_API_ID=your_api_id
TELEGRAM_API_HASH=your_api_hash

# Bluesky
BLUESKY_HANDLE=yourhandle.bsky.social
BLUESKY_APP_PASSWORD=xxxx-xxxx-xxxx-xxxx

# Groq
GROQ_API_KEY=gsk_xxxxxxxxxxxx

# Control Bot
CONTROL_BOT_TOKEN=1234567890:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
ADMIN_TELEGRAM_ID=your_numeric_telegram_id
```

You have two options:

The export tool automatically finds all Telegram channels you're subscribed to and generates a `channels.json`

config file:

```
python "(TOOL)export_telegram_channels.py"
```

On first run, Telethon will ask for your **phone number** and a **login code** sent to Telegram. This creates a local session file so you only need to authenticate once.

The tool will create `channels.json`

with all your channels. Then open it and:

**Set**— Change`destination_channel`

`-100000000000`

to the Telegram channel ID where filterbot should forward approved messages. This must be a channel you own or have admin access to.**Remove channels** you don't want monitored.**Set channel tiers**— Rate each source's credibility:`"high"`

— Official accounts, verified outlets (IDF, Al Jazeera, etc.)`"medium"`

or`""`

— General news aggregators, unverified`"low"`

— Propaganda outlets, known bias sources

**Add rapid-update channels**— Channels that post many updates per minute (e.g., air raid alert bots). These get a lower dedup threshold so legitimate rapid-fire updates aren't dropped.

Copy the template and fill it in manually:

```
cp channels.example.json channels.json
```

Then edit `channels.json`

— see the format in `channels.example.json`

. You can find channel IDs by forwarding a message from the channel to [@userinfobot](https://t.me/userinfobot).

Copy the example filter config:

```
cp filters.example.json filters.json
```

Then edit `filters.json`

to define **what topics your bot monitors**. The file has four sections:

Messages containing any of these words bypass ALL other checks and go straight to the LLM. Use for your most critical, unambiguous signals.

Messages containing any of these are dropped immediately. Use for spam, ads, and off-topic noise.

Messages must contain at least one of these to proceed to the LLM. This is your main topic gate — **add keywords for whatever you're monitoring**.

The system prompt that tells the LLM what "important" means for your use case. Customize this to match your topic.

**Examples for different use cases:**

**Cryptocurrency / DeFi monitoring**

```
{
    "override_keywords": {
        "keywords": ["hack", "exploit", "rug pull", "flash loan attack", "bridge drained", "sec charges"]
    },
    "instant_reject_keywords": {
        "keywords": ["airdrop", "giveaway", "join our group", "buy now", "100x gem", "not financial advice"]
    },
    "must_match_keywords": {
        "keywords": ["bitcoin", "ethereum", "defi", "hack", "exploit", "sec", "regulation", "whale", "liquidation", "stablecoin", "depeg", "exchange", "binance", "coinbase"]
    },
    "llm_prompt": {
        "system_prompt": "You are a crypto intelligence filter. Approve messages about: security exploits, major price movements (>5%), regulatory actions, exchange issues, whale movements. Reject: shilling, price predictions, memes, influencer opinions. Return JSON with 'important' (bool), 'urgency' (1-3), 'reason' (string)."
    }
}
```

**Sports / Football scores**

```
{
    "override_keywords": {
        "keywords": ["goal!", "red card", "penalty", "injury time", "transfer confirmed"]
    },
    "instant_reject_keywords": {
        "keywords": ["bet now", "odds", "prediction", "fantasy", "subscribe"]
    },
    "must_match_keywords": {
        "keywords": ["goal", "score", "match", "transfer", "signed", "injury", "lineup", "suspended", "champions league", "premier league", "red card", "var"]
    },
    "llm_prompt": {
        "system_prompt": "You are a football news filter. Approve: live match events (goals, cards, substitutions), confirmed transfers, injuries, official announcements. Reject: rumours, opinions, betting, fantasy football. Return JSON with 'important' (bool), 'urgency' (1-3), 'reason' (string)."
    }
}
```

**Natural disaster / Weather alerts**

```
{
    "override_keywords": {
        "keywords": ["tsunami warning", "earthquake", "tornado warning", "hurricane", "evacuation order"]
    },
    "instant_reject_keywords": {
        "keywords": ["donate", "pray for", "climate change debate", "subscribe"]
    },
    "must_match_keywords": {
        "keywords": ["earthquake", "tsunami", "tornado", "hurricane", "wildfire", "flood", "evacuation", "magnitude", "category", "storm surge", "landslide", "volcanic"]
    },
    "llm_prompt": {
        "system_prompt": "You are a disaster alert filter. Approve: active natural disaster reports, official warnings, evacuation orders, casualty reports, damage assessments. Reject: general weather forecasts, climate opinions, historical events. Return JSON with 'important' (bool), 'urgency' (1-3), 'reason' (string)."
    }
}
python controlbot.py
```

Then on Telegram, message your control bot:

```
/startall
```

This starts both filterbot and posterbot. On first run, filterbot will prompt in the terminal for your Telegram phone number and a login code.

| Command | Description |
|---|---|
`/status` |
Show running status of all bots |
`/start <bot>` |
Start filterbot or posterbot |
`/stop <bot>` |
Stop a specific bot |
`/restart <bot>` |
Restart a specific bot |
`/startall` |
Start all bots at once |
`/stopall` |
Stop all bots |
`/restartall` |
Restart all bots |
`/logs <bot>` |
View last 50 log lines |
`/stats` |
Latest heartbeat from each bot |
`/clearstats` |
Reset saved heartbeat stats |
`/help` |
List all commands |

```
Incoming message from source channels
    │
    ├── 1. Keyword Pre-Filter (0ms, no API cost)
    │       ├── Override keywords → PASS immediately (e.g. "airstrike", "breaking:")
    │       ├── Instant-reject keywords → DROP (e.g. "donate", "subscribe")
    │       └── Must-match keywords → PASS if signal found, DROP if not
    │
    ├── 2. Semantic Deduplication
    │       └── Compares against last 13 messages using sentence embeddings
    │           Score ≥ 0.65 → duplicate → DROP
    │
    ├── 3. Auto-Translation
    │       └── Non-English messages translated via Google Translate
    │
    ├── 4. LLM Evaluation (Groq — Llama 3.1 8B)
    │       ├── Important → FORWARD with urgency tag
    │       └── Not important → DROP
    │
    └── 5. Forward to destination channel
            ├── 🔴 FLASH — active kinetic events
            ├── 🟡 NOTABLE — significant geopolitical developments
            └── (no tag) — routine newsworthy updates
Message arrives in destination channel
    │
    ├── 1. Semantic Deduplication (with thread override)
    │
    ├── 2. Batch Queue (processes every 60 seconds)
    │
    ├── 3. Image Analysis (if media attached)
    │       ├── Logo detection → drop image if it's a channel logo
    │       └── Auto-captioning → generate caption if no text
    │
    ├── 4. Story Threading Detection
    │       └── Checks if message is an update to a recently posted story
    │           Score 0.55–0.64 → format as "UPDATE:" and reply to original
    │
    ├── 5. AI Formatting (Groq — Llama 3.3 70B)
    │       ├── Standalone → 25-50 word factual summary
    │       └── Thread update → short follow-up referencing original
    │
    └── 6. Post to Bluesky
            ├── Text-only, text + image, or text + video
            └── Thread replies create Bluesky conversation threads
{
    "destination_channel": -100XXXXXXXXXX,
    "channels": {
        "-100XXXXXXXXXX": "Channel Name",
        "-100YYYYYYYYYY": "Another Channel"
    },
    "channel_tiers": {
        "-100XXXXXXXXXX": "high",
        "-100YYYYYYYYYY": ""
    },
    "rapid_update_channels": [
        -100XXXXXXXXXX
    ]
}
```

| Field | Description |
|---|---|
`destination_channel` |
Where filterbot forwards approved messages. Posterbot also watches this channel. |
`channels` |
Map of channel ID → display name. These are the channels filterbot monitors. |
`channel_tiers` |
Credibility rating per channel. Affects how strictly the LLM evaluates messages. |
`rapid_update_channels` |
Channels that get a lower dedup threshold for rapid-fire updates. |

```
{
    "override_keywords": {
        "_comment": "Tier 0: Instant pass — highest confidence signals",
        "keywords": ["breaking:", "flash:", "your critical keyword"]
    },
    "instant_reject_keywords": {
        "_comment": "Tier 1: Instant drop — spam and noise",
        "keywords": ["donate", "subscribe", "your noise keyword"]
    },
    "must_match_keywords": {
        "_comment": "Tier 2: Topic gate — at least one must match",
        "keywords": ["your", "topic", "keywords", "here"]
    },
    "llm_prompt": {
        "_comment": "The AI evaluation prompt — customize for your domain",
        "system_prompt": "Your system prompt here..."
    }
}
```

| Field | Description |
|---|---|
`override_keywords` |
Messages with these words pass instantly. Use for unambiguous, high-priority signals. |
`instant_reject_keywords` |
Messages with these words are dropped. Use for spam, ads, engagement bait. |
`must_match_keywords` |
At least one must match for the message to reach the LLM. This is your main topic filter. |
`llm_prompt` |
The system prompt sent to the AI. Defines what "important" means for your use case. Must instruct the LLM to return JSON with `important` , `urgency` , and `reason` fields. |

Note:All keywords are matched as lowercase substrings. Messages are auto-lowercased before matching, so`"airstrike"`

will match`"AIRSTRIKE"`

,`"Airstrike"`

, etc.

| Parameter | File | Default | Description |
|---|---|---|---|
`SEMANTIC_THRESHOLD` |
filterbot / posterbot | `0.65` |
Similarity score above which messages are considered duplicates |
`UPDATE_THRESHOLD` |
posterbot | `0.55` |
Min similarity to consider a message a story update (for threading) |
`BATCH_INTERVAL` |
posterbot | `60` |
Seconds between batch processing cycles |
`QUEUE_MAX_SIZE` |
posterbot | `10` |
Max messages queued before oldest is dropped |
`LOG_BUFFER_SIZE` |
controlbot | `200` |
Rolling log lines kept per bot |

Both bots emit a heartbeat log every 5 minutes with key metrics:

```
💓 filterbot | ⏱ 2h15m | 📥 847 received | ⚡ 312 passed pre-filter (37%) | ⏭ 28 deduped | ✅ 89 forwarded | ❌ 195 rejected | 🔴 0 Groq fails

💓 posterbot | ⏱ 2h15m | 📥 89 received | ⏭ 3 deduped | 🚀 82 posted | 🧵 7 threaded | ❌ 1 failed | 🔍 4 logos dropped | 📋 Queue: 0/10
```

filterbot also sends a **daily digest** summary to the destination channel every 24 hours.

This project is private. Do not distribute without permission.
