یک قالب برای رباتهای خزنده و اطلاعرسانِ زمانبندیشده: آن را به یک منبع (یک سایت/API که برای آگهیهای جدید بررسی میکنید) وصل میکنید و هر مورد جدید — همراه با جزئیات کامل و هشتگهای خودکار — به هر تعداد گیرنده (تلگرام، بله، روبیکا، ایتا یا هرکدام که خودتان اضافه کنید) ارسال میشود.
بهصورت پیشفرض یک منبع دیوار کاملاً کاربردی دارد، اما لایهٔ منبع و لایهٔ ارسالکنندهها از طریق دو رابط سادهٔ برنامهنویسی (interface) کاملاً از هم جدا شدهاند. یعنی میتوانید این ریپازیتوری را فورک کنید و به یک منبع دیگر وصلش کنید، بدون اینکه لازم باشد منطق ارسال را دست بزنید.
منبع دیوار، نسخهٔ بهشدت تغییریافتهای از debMan/divar-telegram-bot (که خودش از ehcaning/divar-telegram-bot گرفته شده) است. از زمان نوشتهشدن پروژهٔ اصلی، API غیررسمی دیوار تغییر کرده، بنابراین منطق خزیدن اینجا کاملاً متفاوت است.
- روی گیتهاب اکشنز اجرا میشود — نیازی به هاست یا سرور جداگانه نیست. یک ورکفلوی زمانبندیشده ربات را هر چند دقیقه یکبار اجرا میکند.
- جداسازی منبع از گیرنده —
main.pyفقط با یک رابطSourceو لیستی ازSenderها صحبت میکند؛ هیچکدام از وجود دیگری خبر ندارند (به بخش معماری نگاه کنید). - جستوجوی چندشهری (منبع دیوار) — جستوجوی همزمان در چند شهر (
SEARCH_CITY_IDS). - جزئیات کامل آگهی (منبع دیوار) — فیلدهای ساختاریافتهای که خود صفحهٔ آگهی در دیوار نشان میدهد (متراژ، تعداد اتاق، ظرفیت، نرخ شبانه و امکانات) را میگیرد، نه فقط عنوان/قیمت/توضیحات.
- هشتگ خودکار (منبع دیوار) — ترکیبی از هشتگهای مبتنی بر کلیدواژه در متن آگهی و مسیر دستهبندی خود دیوار.
- قالببندی آمادهٔ کانال — عکسها/آلبومها را با کپشن HTML و یک بلوک تماس/فوتر ثابت ارسال میکند، بدون لینک مستقیم خروجی.
- ارسال چندپیامرسانهای — تلگرام عکس/آلبوم کامل دریافت میکند؛ بله، روبیکا و ایتا متن + اولین عکس. وضعیت تحویل هر پلتفرم جدا پیگیری میشود، پس شکست در یک پلتفرم مانع یا باعث تکرار در بقیه نمیشود.
main.py # نقطهٔ ورود - یک Source را به چند Sender وصل میکند و یک بار اجرا میکند
core/
models.py # Item - ساختار عمومی که هر منبع تولید و هر گیرنده مصرف میکند
orchestrator.py # حلقهٔ اصلی: گرفتن شناسههای جدید -> گرفتن هر آیتم -> ارسال -> ذخیرهٔ وضعیت
sources/
base.py # رابط Source: fetch_new_ids(state), fetch_item(id), target_senders
registry.py # متغیر محیطی SOURCE_TYPE -> نمونهٔ Source
divar/ # منبع آماده دیوار (آگهی ساختاریافته -> پیام قالببندیشده)
client.py # DivarSource - پاسخ API دیوار را به Item تبدیل میکند
_raw_client.py # فراخوانیهای سطحپایین API دیوار + پارس کردن
hashtags.py # تولید هشتگ مخصوص دیوار
telegram_relay/ # خودِ تلگرام بهعنوان منبع (به بخش «منبع رله تلگرام» پایینتر نگاه کنید)
client.py # TelegramRelaySource - با getUpdates گوش میدهد و فقط به روبیکا/ایتا رله میکند
website/ # یک سایت گیتهابپیجز بهعنوان منبع (به بخش «منبع سایت» پایینتر نگاه کنید)
client.py # WebsiteSource - فایلهای پوشهٔ submissions/ که فرم سایت کامیت کرده را میخواند
common/ # ابزارهای مشترک بین منابع رلهای (telegram_relay، website)
quotes.py # گرفتن یک نقلقول طبیعت فارسی تصادفی
channel_links.py # ساخت فوتر «ما را جای دیگر هم دنبال کنید»
senders/
base.py # رابط Sender: enabled(), send(item)
registry.py # لیست همهٔ گیرندههای داخلی و فیلتر آنهایی که پیکربندی شدهاند
formatting.py # Item -> متن پیام، مشترک بین همهٔ گیرندهها
telegram.py # از طریق python-telegram-bot
rubika.py # از طریق کتابخانهٔ rubka (تماسهای یکبارهٔ async، بدون حلقهٔ polling)
bale.py, eitaa.py # HTTP خام - هیچ کتابخانهٔ یکبارهٔ مناسبی برای این دو پیدا نشد؛ توضیح پایینتر
http_helpers.py # ابزارهای مشترک HTTP برای گیرندههای شبیه Bot API
text_utils.py # ابزارهای تقسیم متن طولانی به چند پیام
storage.py # وضعیت tokens.json (پیگیری تحویل به تفکیک گیرنده)، مستقل از نوع منبع
config.py # متغیرهای محیطی و ثابتها
requirements.txt
.github/workflows/run-bots.yml
اضافه کردن یک منبع جدید (مثلاً یک سایت آگهی دیگر، یک فید RSS، جستوجوی توییتر): یک sources/<name>/client.py بسازید که یک نمونهٔ SOURCE صادر میکند و کلاسش fetch_new_ids(state) و fetch_item(id) -> Item | None را پیاده میکند. آن را در sources/registry.py ثبت کنید، سپس SOURCE_TYPE=<name> را تنظیم کنید. هیچ بخش دیگری از ریپازیتوری نیاز به تغییر ندارد — همهٔ گیرندهها از قبل با Item صحبت میکنند. اگر منبع شما نیاز به نگهداشتن یک نشانگر/آفست بین اجراها دارد (مثل آفست آپدیتهای telegram_relay)، داخل fetch_new_ids مقدار state["source_state"][self.name] را بخوانید/بنویسید — این مقدار بهطور خودکار در tokens.json ذخیره میشود.
دو نوع محتوای Item: آیتمهای دیوار دادههای ساختاریافتهٔ آگهی هستند (قیمت، مشخصات و...) که گیرندهها آنها را در یک قالب میریزند. همهٔ منابع اینطور نیستند — telegram_relay یک پست ازپیشنوشتهشدهٔ تلگرام را همانطور که هست رله میکند. با تنظیم Item.raw_text، گیرندهها همان متن را عیناً ارسال میکنند، بدون ساختن قالب دیواری دورش.
محدود کردن مقصد ارسال بر اساس منبع: با تنظیم لیست Source.target_senders (مثلاً ["rubika", "eitaa"]) میتوانید مشخص کنید که آیتمهای یک منبع نباید به همهٔ گیرندههای پیکربندیشده بروند — telegram_relay از همین قابلیت استفاده میکند، چون خودِ پست از قبل در تلگرام موجود است. برای ارسال به همهٔ گیرندهها (مثل دیوار) این مقدار را None (پیشفرض) بگذارید.
اضافه کردن یک گیرندهٔ جدید (مثلاً دیسکورد، واتساپ، یک وبهوک): یک senders/<name>.py بسازید با کلاسی که enabled() و async send(item) -> bool را پیاده میکند. یک نمونه از آن را در senders/registry.py ثبت کنید. بهمحض تنظیم متغیرهای محیطی موردنیازش، بهطور خودکار شناسایی و استفاده میشود.
چرا بله و ایتا از HTTP خام استفاده میکنند نه یک کتابخانه: کتابخانهٔ python-bale-bot وجود دارد، اما متد Bot.connect() آن قبل از اینکه نشست HTTP قابلاستفاده شود، یک حلقهٔ polling بینهایت را اجرا میکند - این کتابخانه برای رباتی طراحی شده که دائم در حال اجراست، نه یک اجرای یکبارهٔ کرون، بنابراین استفاده از آن یعنی وابستگی به جزئیات داخلی مستندنشده. برای ایتا هم هیچ کتابخانهٔ نگهداریشدهای وجود ندارد. در مقابل، کتابخانهٔ rubka برای روبیکا تماسهای async یکباره و بدون مرحلهٔ polling دارد، بنابراین گزینهٔ مناسبی است و در rubika.py استفاده شده.
با SOURCE_TYPE=telegram_relay کل ایده برعکس میشود: بهجای خزیدن روی یک سایت آگهی، خودِ ربات تلگرام شما منبع است. یک پست برایش بفرستید — یک عکس یا ویدیو همراه با کپشن، چه بهصورت پیام خصوصی به ربات و چه بهصورت پست کانالی در کانالی که ربات در آن ادمین است — و همان پست به روبیکا و ایتا رله میشود. تلگرام از مقصدهای ارسال حذف میشود، چون پست از قبل همانجا موجود است.
راهاندازی:
- از همان رباتِ مرحلهٔ ۱ استفاده کنید (یا یک ربات جداگانه) - در هر صورت باید
BOT_TOKENتنظیم شده باشد. - برای رله کردن پستهای کانال: ربات را بهعنوان ادمین کانال اضافه کنید (کانال ← مدیران ← افزودن مدیر). نیازی به دسترسی خاصی فراتر از خواندن پیامها ندارد.
- شناسهٔ عددی چتهایی که میخواهید پست از آنها پذیرفته شود را پیدا کنید:
- شناسهٔ کاربری خودتان، برای پیامدادن مستقیم به ربات — به
@userinfobotپیام دهید. - شناسهٔ عددی کانال (چیزی شبیه
-1001234567890) — یک پیام از کانال را به@userinfobotفوروارد کنید، یا بعد از یکبار پست کردن، پاسخgetUpdatesربات را بررسی کنید.
- شناسهٔ کاربری خودتان، برای پیامدادن مستقیم به ربات — به
- مقدار
TELEGRAM_RELAY_CHAT_IDSرا برابر لیستی از این شناسهها با کاما جدا کنید (مثلاً123456789,-1001234567890). این مقدار الزامی است — بدون آن، منبع هیچچیزی را پردازش نمیکند، تا یک پیام خصوصی ناخواسته از یک نفر دیگر به کانالهای شما رله نشود. - مقدار
SOURCE_TYPE=telegram_relayرا بهعنوان یک سیکرت ریپازیتوری تنظیم کنید.
نقلقول طبیعت + فوتر لینک کانالها: هر پست رلهشده یک نقلقول تصادفی با موضوع طبیعت دریافت میکند (در لحظهٔ اجرا از فایل موضوعی tabiat.json در ریپازیتوری aliaslany/persian-quotes گرفته میشود، بدون نیاز به داخلریپو بودن دادهها)، بههمراه یک فوتر «ما را جای دیگر هم دنبال کنید» که به همان محتوا در کانالهای تلگرام/بله/روبیکا لینک میدهد. هر گیرنده این لینکها را در همان قالبی که آن پلتفرم واقعاً پشتیبانی میکند رندر میکند — لینک واقعاً کلیکپذیر در روبیکا (از طریق تبدیل HTML به متادیتای لینک روبیکا)، و متن ساده به شکل برچسب: آدرس در ایتا (چون ایتا از لینک غنی پشتیبانی نمیکند). اینها را میتوانید از طریق CHANNEL_LINK_LABEL، TELEGRAM_CHANNEL_URL، BALE_CHANNEL_URL، RUBIKA_CHANNEL_URL و NATURE_QUOTES_URL تنظیم کنید (جدول سیکرتها را پایینتر ببینید) — هر کدام از *_CHANNEL_URL را خالی بگذارید تا آن پلتفرم از فوتر حذف شود.
محدودیت شناختهشده: تلگرام هر عکس از یک آلبوم چندعکسی را بهصورت یک آپدیت جداگانه میفرستد. این منبع فعلاً هر پیام را یک پست مستقل در نظر میگیرد، پس یک آلبوم چندعکسی به چند پست جدا در روبیکا/ایتا تبدیل میشود، نه یک آلبوم گروهبندیشده. برای پستهای تکعکس/تکویدیو مشکلی ندارد؛ اگر آلبوم زیاد پست میکنید، گروهبندی بر اساس media_group_id قدم بعدی طبیعی خواهد بود.
با SOURCE_TYPE=website، بهجای تلگرام از فرم گیتهابپیجزِ داخل پوشهٔ docs/ بهعنوان منبع استفاده میشود. صفحه را باز کنید، یک متن بنویسید، یک عکس یا ویدیو پیوست کنید و ارسال کنید — فرم هر دو فایل را مستقیماً با استفاده از توکن گیتهاب خودتان (که در مرورگر وارد میشود، هیچجای دیگری ذخیره نمیشود، مگر اینکه گزینهٔ بهخاطرسپاری را در همان مرورگر تیک بزنید) در پوشهٔ submissions/ همین ریپازیتوری کامیت میکند. چون گیتهاب اکشنز همیشه قبل از اجرا کل ریپازیتوری را چکاوت میکند، اجرای زمانبندیشدهٔ بعدی فقط همان فایلها را از روی دیسک میخواند — نیازی به هیچ API پولینگی در سمت ربات نیست.
برخلاف telegram_relay، این محتوا هنوز جایی وجود ندارد، پس target_senders برابر None است — یعنی به همهٔ گیرندههایی که پیکربندی کردهاید (تلگرام، بله، روبیکا، ایتا) ارسال میشود. همان رفتار نقلقول طبیعت + فوتر لینک کانالها که در telegram_relay توضیح داده شد اینجا هم اعمال میشود، چون هر دو از همان ابزارهای مشترک در sources/common/ استفاده میکنند.
راهاندازی:
- گیتهابپیجز را فعال کنید: Settings → Pages → Source ← «Deploy from a branch» ← شاخهٔ
main، پوشهٔ/docs. یک دقیقه صبر کنید تا اولین استقرار انجام شود، سپس فرم شما در آدرسhttps://<username>.github.io/<repo>/در دسترس خواهد بود. - مقدار
SOURCE_TYPE=websiteرا بهعنوان یک سیکرت ریپازیتوری تنظیم کنید. - هر بار که از فرم استفاده میکنید، یک توکن دسترسی شخصی fine-grained بسازید که فقط به همین ریپازیتوری محدود باشد و دسترسی Contents: Read and write داشته باشد (
https://github.com/settings/tokens?type=beta) و آن را در فیلد توکن فرم وارد کنید. این توکن فقط برای همان دو فراخوانی API که پست شما را کامیت میکند استفاده میشود — به جای دیگری ارسال نمیشود.
نکته دربارهٔ پاکسازی: فایلهای هر پست بلافاصله بعد از خواندهشدن حذف میشوند، قبل از اینکه تحویل موفق به همهٔ پلتفرمها تأیید شده باشد. اگر ارسال در وسط راه شکست بخورد، محتوا از بین میرود و بهطور خودکار دوباره امتحان نمیشود — این قابلقبول است چون برخلاف یک آگهی دیوار، میتوانید دوباره از طریق فرم ارسالش کنید. مرحلهٔ کامیت وضعیت در ورکفلو، این حذفها را هم همراه با tokens.json به ریپازیتوری پوش میکند.
چون هاست رایگان جایی برای اجرای یک پردازش دائمی نمیدهد، ربات بهطور پیوسته اجرا نمیشود. در عوض، یک ورکفلوی گیتهاب اکشنز آن را طبق زمانبندی (مثلاً هر ۱۰ دقیقه) اجرا میکند. هر اجرا:
- آیتمهای جدید را از منبع پیکربندیشده میگیرد (آگهیهای دیوار، یا پیامهای تلگرام برای
telegram_relay). - هر آیتم جدید را به هر گیرندهای که آن منبع اجازه میدهد ارسال میکند (به
Source.target_sendersنگاه کنید). - وضعیت بهروزشده (
tokens.json) را به ریپازیتوری کامیت میکند تا اجرای بعدی از همانجا ادامه دهد.
@BotFather را در تلگرام باز کنید، یک ربات بسازید و توکن آن را یادداشت کنید.
- چت خصوصی: به ربات پیام بدهید، سپس آدرس
https://api.telegram.org/bot<TOKEN>/getUpdatesرا باز کنید و مقدارchat.idرا بخوانید. - کانال عمومی: میتوانید مستقیماً از
@usernameآن بهعنوان شناسهٔ چت استفاده کنید. - کانال/گروه خصوصی: ربات را بهعنوان ادمین با دسترسی «ارسال پیام» اضافه کنید، یک پیام در آن بفرستید، سپس
getUpdatesرا همانطور بررسی کنید — شناسه یک عدد منفی بزرگ خواهد بود.
به divar.ir بروید، شهر و دستهبندی موردنظر را انتخاب کنید و در حین مرور نتایج جستوجو، تب Network مرورگر (DevTools) را باز کنید. مقادیر city_ids و category را در درخواست ارسالی به api.divar.ir/v8/postlist/w/search پیدا کنید. بهطور جایگزین، آدرسی که هنگام مرور divar.ir/s/... نشان داده میشود اغلب همان اسلاگ دستهبندی را نشان میدهد (مثل real-estate, villa, temporary-rent).
در فورک خودتان به Settings → Secrets and variables → Actions بروید و موارد زیر را اضافه کنید:
| سیکرت | الزامی؟ | مثال | توضیح |
|---|---|---|---|
BOT_TOKEN |
اختیاری | 123456:ABC-DEF... |
توکن ربات تلگرام از BotFather |
BOT_CHATID |
اختیاری | -1001234567890 یا @mychannel |
چت/کانال مقصد در تلگرام |
BALE_BOT_TOKEN |
اختیاری | توکن ربات بله | |
BALE_CHATID |
اختیاری | چت/کانال مقصد در بله | |
RUBIKA_BOT_TOKEN |
اختیاری | توکن ربات روبیکا | |
RUBIKA_CHATID |
اختیاری | چت/کانال مقصد در روبیکا | |
EITAA_TOKEN |
اختیاری | توکن API ایتایار | |
EITAA_CHATID |
اختیاری | چت/کانال مقصد در ایتا | |
SOURCE_TYPE |
اختیاری | divar |
کدام منبع بررسی شود (به sources/registry.py نگاه کنید)؛ پیشفرض divar |
TELEGRAM_RELAY_CHAT_IDS |
برای telegram_relay الزامی |
123456789,-1001234567890 |
شناسههای چتی که اجازهٔ پست از طریق رله را دارند (به منبع رله تلگرام نگاه کنید) |
CHANNEL_LINK_LABEL |
اختیاری | طبیعت+ |
برچسب کلیکپذیر برای هر لینک فوتر «ما را جای دیگر هم دنبال کنید» |
TELEGRAM_CHANNEL_URL |
اختیاری | https://t.me/nature_plus |
لینک فوتر به کانال تلگرام شما؛ خالی بگذارید تا حذف شود |
BALE_CHANNEL_URL |
اختیاری | https://ble.ir/natureplus |
لینک فوتر به کانال بله شما؛ خالی بگذارید تا حذف شود |
RUBIKA_CHANNEL_URL |
اختیاری | https://rubika.ir/natureplus1 |
لینک فوتر به کانال روبیکای شما؛ خالی بگذارید تا حذف شود |
NATURE_QUOTES_URL |
اختیاری | آدرس jsDelivr برای tabiat.json |
برای استفاده از یک دیتاست/موضوع نقلقول دیگر تغییرش دهید |
GITHUB_REPO |
در اکشنز خودکار تنظیم میشود | aliaslany/Multi_sender |
برای ساخت آدرس فایل خام در منبع website استفاده میشود؛ فقط برای توسعهٔ محلی خارج از اکشنز نیاز به تنظیم دستی دارد |
SEARCH_CITY_IDS |
✅ | 823,1996,1999 |
شناسههای عددی شهر با کاما جدا (فقط منبع دیوار) |
SEARCH_CATEGORY |
✅ | real-estate |
اسلاگ دستهبندی دیوار |
PROXY_URL |
اختیاری | فقط اگر رانر شما نمیتواند مستقیم به دیوار/تلگرام دسترسی داشته باشد |
حداقل یک جفت توکن/شناسهٔ چت را تنظیم کنید. تلگرام ارسال کامل عکس یا آلبوم را حفظ میکند؛ بله اولین عکس آگهی و سپس متن قالببندیشده را میفرستد؛ روبیکا و ایتا آگهی قالببندیشده را بهصورت متن دریافت میکنند. کلاینتهای غیر از تلگرام از اندپوینتهای سازگار با Bot API استفاده میکنند و با متغیرهای محیطی اختیاری BALE_API_BASE_URL، RUBIKA_API_BASE_URL یا EITAA_API_BASE_URL میتوان آنها را به گیتویهای جایگزین وصل کرد.
tokens.json اکنون تحویل هر پلتفرم را جداگانه ثبت میکند. اگر یک پلتفرم شکست بخورد، اجرای بعدی فقط همان پلتفرم را دوباره امتحان میکند و از پست تکراری در پلتفرمهایی که موفق بودهاند جلوگیری میشود.
Settings → Actions → General → Workflow permissions ← گزینهٔ «Read and write permissions» را انتخاب کنید (لازم است تا ورکفلو بتواند tokens.json را به ریپازیتوری کامیت کند).
به تب Actions بروید ← ورکفلو را انتخاب کنید ← Run workflow. پس از موفقیت، طبق زمانبندی تعریفشده در .github/workflows/run-bots.yml بهطور خودکار اجرا میشود.
git clone https://github.com/aliaslany/Multi_sender.git
cd Multi_sender
pip install -r requirements.txt
cp .env.example .env
# مقادیر .env را با توکنها/شناسههای واقعی خودتان پر کنید
export $(grep -v '^#' .env | xargs)
echo '{}' > tokens.json
python main.pyیا با داکر:
docker compose up --build- این پروژه از API غیررسمی دیوار (همان چیزی که خودِ divar.ir صدا میزند) استفاده میکند که از ترافیک مرورگر مهندسی معکوس شده است. اگر دیوار هدرها، اندپوینتها یا ساختار پاسخ را تغییر دهد، ممکن است دوباره بشکند.
- تشخیص هشتگ بر اساس کلیدواژه/زیررشته است، پس عبارتهای غیرمعمول در متن آگهی ممکن است دیده نشوند.
telegram_relayهر پیام تلگرام را یک پست مستقل در نظر میگیرد، پس یک آلبوم چندعکسی به چند پست جدا در پلتفرمهای مقصد تبدیل میشود، نه یک آلبوم گروهبندیشده.websiteفایلهای هر پست را بلافاصله بعد از خواندن حذف میکند، قبل از تأیید تحویل موفق به همهٔ پلتفرمها — یک ارسال ناموفق باعث ازدسترفتن محتوا میشود، نه تلاش خودکار دوباره.
به پروژهٔ اصلی بالادستی نگاه کنید — در این فورک مجوز جداگانهای اضافه نشده است.