A scheduled crawler and notifier for Divar (Iran's largest classifieds app) that watches one or more cities/categories for new listings and posts them — with rich details, auto-generated hashtags, and optional "likely sold/rented" alerts — to a Telegram chat or channel.
This is a heavily modified fork of debMan/divar-telegram-bot (originally ehcaning/divar-telegram-bot). Divar changed its unofficial API since the original project was written, so the crawling logic, project structure, and feature set here are substantially different.
- Runs on GitHub Actions — no server to host or pay for. A scheduled workflow runs the bot every few minutes.
- Multi-city search — search across several cities at once (
SEARCH_CITY_IDS). - Rich ad details — pulls structured fields Divar shows on the ad page (area, room count, capacity, nightly rates, amenities, etc.), not just title/price/description.
- Auto-generated hashtags — combines:
- keyword-based tags detected in the ad text (property type, known local areas, deal type)
- Divar's own breadcrumb category chain (e.g.
#اجارهٔ_کوتاه_مدت_ویلا_و_باغ)
- Channel-ready formatting — sends photos/albums with an HTML-formatted caption and a fixed contact/footer block, no direct outbound link.
- "Likely sold/rented" alerts (optional, heuristic) — periodically rechecks previously-posted ads and notifies the channel if one seems to have disappeared from Divar.
- Admin-controlled filters (optional) — authorized admins can DM the bot commands to change which cities/category it searches, without touching the repo.
main.py # entry point / orchestration
config.py # env vars and constants (search filters, footer text, etc.)
divar_client.py # talks to Divar's API, extracts ad fields
hashtags.py # keyword + breadcrumb hashtag generation
telegram_client.py # Telegram formatting and rich-media delivery
messenger_client.py # Bale, Rubika, and Eitaa text delivery
storage.py # tokens.json state (per-messenger delivery tracking)
admin_commands.py # optional: admin DM commands for changing filters
status_checker.py # optional: re-checks old ads for removal
requirements.txt
.github/workflows/run-bot.yml
Because free hosting doesn't give you a place to run a long-lived process, the bot doesn't run continuously. Instead, a GitHub Actions workflow runs it on a schedule (e.g. every 10 minutes). Each run:
- Polls for any pending admin DM commands (if
ADMIN_USER_IDSis set) and updates search filters accordingly. - Searches Divar for the configured cities/category, sorted by newest.
- Sends every new ad to each configured messenger.
- Optionally rechecks a batch of older ads to see if they look removed, and announces those.
- Commits the updated state (
tokens.json, andfilters.json/admin_state.jsonif used) back to the repo, so the next run picks up where this one left off.
Open @BotFather in Telegram, create a bot, and note its token.
- Private chat: message your bot, then visit
https://api.telegram.org/bot<TOKEN>/getUpdatesand read thechat.idfield. - Public channel: you can just use its
@usernamedirectly as the chat ID. - Private channel/group: add the bot as an admin with "Post Messages" permission, send a message in it, then check
getUpdatesthe same way — the ID will be a large negative number.
Go to divar.ir, pick your city and category, and open the browser's Network tab (DevTools) while browsing search results. Look for the city_ids and category values in the request sent to api.divar.ir/v8/postlist/w/search. Alternatively, the URL shown when browsing divar.ir/s/... often reflects the category slug (e.g. real-estate, villa, temporary-rent).
Go to Settings → Secrets and variables → Actions in your fork and add:
| Secret | Required | Example | Notes |
|---|---|---|---|
BOT_TOKEN |
optional | 123456:ABC-DEF... |
Telegram bot token from BotFather |
BOT_CHATID |
optional | -1001234567890 or @mychannel |
Telegram destination chat/channel |
BALE_BOT_TOKEN |
optional | Bale bot token | |
BALE_CHATID |
optional | Bale destination chat/channel | |
RUBIKA_BOT_TOKEN |
optional | Rubika bot token | |
RUBIKA_CHATID |
optional | Rubika destination chat/channel | |
EITAA_TOKEN |
optional | EitaaYar API token | |
EITAA_CHATID |
optional | Eitaa destination chat/channel | |
SEARCH_CITY_IDS |
✅ | 823,1996,1999 |
Comma-separated numeric city IDs |
SEARCH_CATEGORY |
✅ | real-estate |
Divar category slug |
PROXY_URL |
optional | Only needed if your runner can't reach Divar/Telegram directly | |
ADMIN_USER_IDS |
optional | 111111,222222 |
Telegram numeric user IDs allowed to change filters via DM (see below) |
STATUS_CHECK_LIMIT |
optional | 20 |
Max old ads rechecked per run for the "likely removed" feature |
Configure at least one token/chat-ID pair. Telegram keeps its rich photo or album
delivery. Bale sends the first listing image followed by the formatted ad text; Rubika
and Eitaa receive the formatted ad as text. The non-Telegram clients use compatible
Bot API endpoints and can be pointed at alternative gateways with the optional
BALE_API_BASE_URL, RUBIKA_API_BASE_URL, or EITAA_API_BASE_URL environment
variables.
tokens.json now records delivery per platform. If one platform fails, the next run
retries only that platform, avoiding duplicate posts on the platforms that succeeded.
Settings → Actions → General → Workflow permissions → select "Read and write permissions" (needed so the workflow can commit tokens.json back to the repo).
Go to the Actions tab → select the workflow → Run workflow. On success it'll run automatically on the schedule defined in .github/workflows/run-bot.yml.
If ADMIN_USER_IDS is set, authorized users can DM the bot (private chat, not the channel):
/set_cities 823,1996,1999
/set_category real-estate
/show_filters
/help
Changes take effect starting the next scheduled run and only affect future searches — the bot never edits or deletes messages it already sent.
git clone https://github.com/<your-username>/divar-teleg-bot.git
cd divar-teleg-bot
pip install -r requirements.txt
export BOT_TOKEN=...
export BOT_CHATID=...
export SEARCH_CITY_IDS=823,1996
export SEARCH_CATEGORY=real-estate
echo '{}' > tokens.json
python main.py- This uses Divar's unofficial web API (the same one divar.ir itself calls), reverse-engineered from browser traffic. It can break again if Divar changes headers, endpoints, or response shapes.
- The "likely sold/rented" detection is a heuristic (an ad becoming unreachable), not an explicit status field from Divar, since none is exposed on this endpoint. It can occasionally misfire; see
status_checker.pyfor details and a debug flag to help refine it. - Hashtag detection is keyword/substring-based, so unusual phrasing in an ad's text may be missed.
See the original upstream project — no separate license has been added in this fork.