feat: add source websites with automated endpoint analysis, target queue dispatch ordering, and markdown styling
This commit is contained in:
+310
-6
@@ -46,10 +46,11 @@ def get_persian_main_menu(is_paused: bool = False):
|
||||
return [
|
||||
[Button.text("📊 آمار و وضعیت ناوگان", resize=True), Button.text("🔑 درخواست کد لاگین", resize=True)],
|
||||
[Button.text("📡 کانالهای مبدا", resize=True), Button.text("🎯 کانالهای مقصد", resize=True)],
|
||||
[Button.text("📂 دستهبندی کانالها", resize=True), Button.text("🧠 تنظیمات و لاگهای AI", resize=True)],
|
||||
[Button.text("🌐 وبسایتهای مبدا", resize=True), Button.text("📂 دستهبندی کانالها", resize=True)],
|
||||
[Button.text("🧠 تنظیمات و لاگهای AI", resize=True), Button.text("⚠️ خطاهای سیستم", resize=True)],
|
||||
[Button.text("➕ افزودن کانال مبدا", resize=True), Button.text("➕ افزودن کانال مقصد", resize=True)],
|
||||
[Button.text("📨 ارسال پستهای بررسینشده", resize=True), Button.text("⚠️ خطاهای سیستم", resize=True)],
|
||||
[Button.text(pause_btn, resize=True), Button.text("❓ راهنمای سیستم", resize=True)]
|
||||
[Button.text("📨 ارسال پستهای بررسینشده", resize=True), Button.text(pause_btn, resize=True)],
|
||||
[Button.text("❓ راهنمای سیستم", resize=True)]
|
||||
]
|
||||
|
||||
|
||||
@@ -120,6 +121,23 @@ class AdminBotService:
|
||||
def set_queue(self, queue: RedisQueue):
|
||||
self.queue = queue
|
||||
|
||||
def set_website_services(self, analyzer, collector):
|
||||
self.website_analyzer = analyzer
|
||||
self.website_collector = collector
|
||||
|
||||
async def on_website_error_alert(self, alert_text: str, site_id: int):
|
||||
buttons = [[Button.inline("🤖 تحلیل مجدد هوشمند وبسایت", data=f"web_reanalyze:{site_id}")]]
|
||||
if self.review_channel_id:
|
||||
try:
|
||||
await self.client.send_message(self.review_channel_id, alert_text, parse_mode="html", buttons=buttons)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send website alert to review channel: {e}")
|
||||
for admin_id in self.admin_user_ids:
|
||||
try:
|
||||
await self.client.send_message(admin_id, alert_text, parse_mode="html", buttons=buttons)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def is_admin(self, user_id: int) -> bool:
|
||||
return not self.admin_user_ids or user_id in self.admin_user_ids
|
||||
|
||||
@@ -250,12 +268,16 @@ class AdminBotService:
|
||||
cat_name = cat.name
|
||||
|
||||
custom_p = getattr(target, "custom_prompt", "") or ""
|
||||
order_mode = getattr(target, "dispatch_order", "order") or "order"
|
||||
order_label = "🟢 به ترتیب ورود (FIFO)" if order_mode == "order" else "🎲 تصادفی (Random)"
|
||||
|
||||
card_text = (
|
||||
f"🎯 <b>تنظیمات کانال مقصد:</b> <b>{target.title}</b>\n\n"
|
||||
f"• 🆔 <b>شناسه کانال:</b> <code>{target.channel_id}</code> (ID: <code>{target.id}</code>)\n"
|
||||
f"• 🔗 <b>یوزرنیم:</b> @{target.username or 'ندارد'}\n"
|
||||
f"• 📁 <b>دستهبندی:</b> <b>{cat_name}</b>\n"
|
||||
f"• 🌐 <b>زبان کانال:</b> <b>{lang_label}</b>\n"
|
||||
f"• 🔀 <b>ترتیب ارسال از صف:</b> <b>{order_label}</b>\n"
|
||||
f"• ⏱ <b>فاصله ارسال پستها:</b> هر <b>{target.post_interval_min} دقیقه</b>\n"
|
||||
f"• 🌙 <b>وضعیت ساعت خواب:</b> <b>{sleep_st}</b>\n"
|
||||
f"• 📥 <b>پستهای منتظر در صف:</b> <b>{qsize} پست</b>\n"
|
||||
@@ -280,10 +302,13 @@ class AdminBotService:
|
||||
],
|
||||
[
|
||||
Button.inline("🌐 تنظیم زبان", data=f"st_lang:{target.id}"),
|
||||
Button.inline("⏱ تغییر فاصله ارسال", data=f"st_intv:{target.id}")
|
||||
Button.inline(f"🔀 شیوه ارسال ({'ترتیبی' if order_mode == 'order' else 'تصادفی'})", data=f"trg_order:{target.id}")
|
||||
],
|
||||
[
|
||||
Button.inline("⏱ تغییر فاصله ارسال", data=f"st_intv:{target.id}"),
|
||||
Button.inline("🌙 تنظیم ساعت خواب", data=f"st_slp:{target.id}")
|
||||
],
|
||||
[
|
||||
Button.inline("🌙 تنظیم ساعت خواب", data=f"st_slp:{target.id}"),
|
||||
Button.inline("🤖 ارسال خودکار از مبدا", data=f"auto_src:{target.id}")
|
||||
]
|
||||
]
|
||||
@@ -905,6 +930,93 @@ class AdminBotService:
|
||||
]
|
||||
return text, buttons
|
||||
|
||||
async def _render_website_list(self) -> Tuple[str, List[List[Button]]]:
|
||||
sites = await self.repo.get_source_websites()
|
||||
if not sites:
|
||||
text = (
|
||||
"🌐 <b>لیست وبسایتهای مبدا (Source Websites)</b>\n\n"
|
||||
"هنوز هیچ وبسایت مبدایی ثبت نشده است.\n"
|
||||
"شما میتوانید هر وبسایت، خبرگزاری یا وبلاگی را به عنوان مبدا ثبت کنید تا هوش مصنوعی بهطور خودکار ساختار آن را تحلیل و محتوای جدید را استخراج کند."
|
||||
)
|
||||
buttons = [
|
||||
[Button.inline("➕ افزودن وبسایت مبدا جدید", data="add_web")],
|
||||
[Button.inline("🔙 بازگشت به منوی اصلی", data="back_to_main")]
|
||||
]
|
||||
return text, buttons
|
||||
|
||||
text = f"🌐 <b>لیست وبسایتهای مبدا ({len(sites)} وبسایت):</b>\n\nبرای مشاهده تنظیمات، دریافت دستی یا تحلیل مجدد روی وبسایت کلیک کنید:"
|
||||
buttons = []
|
||||
for s in sites:
|
||||
status_icon = "🔴" if s.last_error else "🟢"
|
||||
buttons.append([Button.inline(f"{status_icon} {s.name} ({s.check_interval_min}m)", data=f"web_view:{s.id}")])
|
||||
buttons.append([Button.inline("➕ افزودن وبسایت مبدا جدید", data="add_web")])
|
||||
buttons.append([Button.inline("🔙 بازگشت به منوی اصلی", data="back_to_main")])
|
||||
return text, buttons
|
||||
|
||||
async def _render_website_config(self, site_id: int) -> Tuple[str, List[List[Button]]]:
|
||||
site = await self.repo.get_source_website_by_id(site_id)
|
||||
if not site:
|
||||
return "❌ وبسایت یافت نشد.", []
|
||||
|
||||
cat_name = "بدون دستهبندی"
|
||||
if site.category_id:
|
||||
cat = await self.repo.get_category_by_id(site.category_id)
|
||||
if cat:
|
||||
cat_name = cat.name
|
||||
|
||||
api_cfg = site.api_config or {}
|
||||
parser_type = api_cfg.get("parser_type", "نامشخص")
|
||||
endpoint = api_cfg.get("endpoint_url", site.url)
|
||||
err_block = f"⚠️ <b>خطای اخیر:</b> <code>{site.last_error}</code>\n\n" if site.last_error else ""
|
||||
last_fe = site.last_fetched_at.strftime("%Y-%m-%d %H:%M") if site.last_fetched_at else "هنوز دریافت نشده"
|
||||
last_re = site.last_reanalyzed_at.strftime("%Y-%m-%d %H:%M") if site.last_reanalyzed_at else "انجام نشده"
|
||||
|
||||
text = (
|
||||
f"🌐 <b>وبسایت مبدا:</b> <b>{site.name}</b>\n\n"
|
||||
f"• 🔗 <b>آدرس وبسایت:</b> {site.url}\n"
|
||||
f"• 🔌 <b>اندپوینت دریافت:</b> <code>{endpoint}</code>\n"
|
||||
f"• 🏷 <b>نوع ساختار:</b> <code>{parser_type}</code>\n"
|
||||
f"• 📁 <b>دستهبندی:</b> <b>{cat_name}</b>\n"
|
||||
f"• ⏱ <b>فاصله بررسی:</b> هر <b>{site.check_interval_min} دقیقه</b>\n"
|
||||
f"• 🔄 <b>دوره تحلیل مجدد AI:</b> هر <b>{site.auto_reanalyze_hours} ساعت</b>\n"
|
||||
f"• 📥 <b>آخرین دریافت:</b> <code>{last_fe}</code>\n"
|
||||
f"• 🧠 <b>آخرین تحلیل هوش مصنوعی:</b> <code>{last_re}</code>\n\n"
|
||||
f"{err_block}"
|
||||
f"<i>👇 عملیات مورد نظر را انتخاب کنید:</i>"
|
||||
)
|
||||
buttons = [
|
||||
[
|
||||
Button.inline("🔄 دریافت دستی (Fetch Now)", data=f"web_fetch:{site.id}"),
|
||||
Button.inline("🤖 تحلیل مجدد هوشمند AI", data=f"web_reanalyze:{site.id}")
|
||||
],
|
||||
[
|
||||
Button.inline("⏱ تغییر فاصله بررسی", data=f"web_intv:{site.id}"),
|
||||
Button.inline("🔄 دوره تحلیل مجدد AI", data=f"web_reintv:{site.id}")
|
||||
],
|
||||
[
|
||||
Button.inline("📁 تعیین دستهبندی", data=f"web_cat:{site.id}"),
|
||||
Button.inline("🗑 حذف این وبسایت", data=f"del_web:{site.id}")
|
||||
],
|
||||
[
|
||||
Button.inline("🔙 بازگشت به لیست وبسایتها", data="list_web")
|
||||
]
|
||||
]
|
||||
return text, buttons
|
||||
|
||||
async def _render_website_category_menu(self, site_id: int):
|
||||
site = await self.repo.get_source_website_by_id(site_id)
|
||||
if not site:
|
||||
return "❌ وبسایت یافت نشد.", []
|
||||
cats = await self.repo.get_categories()
|
||||
text = f"📁 <b>انتخاب دستهبندی برای وبسایت «{site.name}»:</b>"
|
||||
buttons = []
|
||||
for c in cats:
|
||||
mark = "✅ " if site.category_id == c.id else ""
|
||||
buttons.append([Button.inline(f"{mark}{c.name}", data=f"web_set_cat:{site.id}:{c.id}")])
|
||||
buttons.append([Button.inline("❌ بدون دستهبندی", data=f"web_set_cat:{site.id}:none")])
|
||||
buttons.append([Button.inline("🔙 بازگشت به تنظیمات وبسایت", data=f"web_view:{site.id}")])
|
||||
return text, buttons
|
||||
|
||||
|
||||
async def _render_auto_sources(self, target_id: int):
|
||||
"""Toggle screen listing every source with its on/off state for this target."""
|
||||
@@ -1217,6 +1329,14 @@ class AdminBotService:
|
||||
text, buttons = await self._render_target_list()
|
||||
await event.reply(text, parse_mode="html", buttons=buttons or None)
|
||||
|
||||
# --- Source Websites ---
|
||||
@self.client.on(events.NewMessage(pattern=r"(?i)^(/websites|/sites|🌐 وبسایتهای مبدا)$"))
|
||||
async def cmd_websites(event: events.NewMessage.Event):
|
||||
if not self.is_admin(event.sender_id):
|
||||
return
|
||||
text, buttons = await self._render_website_list()
|
||||
await event.reply(text, parse_mode="html", buttons=buttons or None)
|
||||
|
||||
# --- Categories Management ---
|
||||
@self.client.on(events.NewMessage(pattern=r"(?i)^(/categories|/cats|📂 دستهبندی کانالها)$"))
|
||||
async def cmd_categories(event: events.NewMessage.Event):
|
||||
@@ -1333,9 +1453,23 @@ class AdminBotService:
|
||||
return "📢 <b>گزارش تغییرات سیستم (Release Notes):</b>\nسیستم با آخرین بهروزرسانیها در حال اجرا است."
|
||||
|
||||
async def broadcast_change_notes(self):
|
||||
"""Send latest change notes to admins and review channel."""
|
||||
"""Send latest change notes to admins and review channel if they are new."""
|
||||
import hashlib
|
||||
notes = self.get_latest_change_notes()
|
||||
current_hash = hashlib.sha256(notes.encode("utf-8")).hexdigest()
|
||||
last_hash = await self.repo.get_setting("last_broadcast_release_hash")
|
||||
|
||||
if last_hash == current_hash:
|
||||
logger.info("Release notes have not changed since last broadcast. Skipping automatic broadcast.")
|
||||
return
|
||||
|
||||
logger.info(f"Broadcasting new release notes (hash: {current_hash[:8]})...")
|
||||
await self.notify_admins(notes)
|
||||
await self.repo.set_setting(
|
||||
"last_broadcast_release_hash",
|
||||
current_hash,
|
||||
description="Hash of last broadcast release notes"
|
||||
)
|
||||
|
||||
|
||||
# --- Step-by-Step Parameter & Text Input Message Handler ---
|
||||
@@ -1593,6 +1727,73 @@ class AdminBotService:
|
||||
menu = await self.get_menu()
|
||||
await event.reply(res, parse_mode="html", buttons=menu)
|
||||
|
||||
# 8.1. Waiting for Website Input (Name and URL)
|
||||
elif action == "wait_website_input":
|
||||
self.user_states.pop(event.sender_id, None)
|
||||
parts = text.split()
|
||||
if len(parts) < 2:
|
||||
menu = await self.get_menu()
|
||||
await event.reply("❌ فرمت نامعتبر است. لطفاً نام و آدرس را با فاصله ارسال کنید (مثال: <code>دیجیاتو https://digiato.com</code>).", parse_mode="html", buttons=menu)
|
||||
return
|
||||
|
||||
# If last part is URL:
|
||||
if parts[-1].startswith(("http://", "https://", "www.")) or "." in parts[-1]:
|
||||
url = parts[-1]
|
||||
name = " ".join(parts[:-1])
|
||||
else:
|
||||
url = parts[0]
|
||||
name = " ".join(parts[1:])
|
||||
|
||||
if not url.startswith("http://") and not url.startswith("https://"):
|
||||
url = "https://" + url
|
||||
|
||||
loading_msg = await event.reply(
|
||||
f"🤖 <b>در حال تحلیل وبسایت «{name}» ({url}) توسط هوش مصنوعی...</b>\n<i>(کشف خودکار REST APIها، فیدهای RSS و ساختار داده)</i>",
|
||||
parse_mode="html"
|
||||
)
|
||||
|
||||
api_cfg = {}
|
||||
if self.website_analyzer:
|
||||
ok, cfg, summary = await self.website_analyzer.analyze_website(url)
|
||||
if ok and cfg:
|
||||
api_cfg = cfg
|
||||
|
||||
site_id = await self.repo.add_source_website(name=name, url=url, api_config=api_cfg)
|
||||
card, buttons = await self._render_website_config(site_id)
|
||||
await loading_msg.edit(
|
||||
f"✅ <b>وبسایت «{name}» با موفقیت افزوده و پیکربندی شد!</b>\n\n{card}",
|
||||
parse_mode="html",
|
||||
buttons=buttons
|
||||
)
|
||||
|
||||
# 8.2. Waiting for Website Fetch Interval
|
||||
elif action == "wait_website_intv":
|
||||
site_id = state.get("site_id")
|
||||
self.user_states.pop(event.sender_id, None)
|
||||
try:
|
||||
val = int(text.strip())
|
||||
await self.repo.update_source_website_interval(site_id, val)
|
||||
card, buttons = await self._render_website_config(site_id)
|
||||
await event.reply(f"✅ فاصله بررسی وبسایت روی هر <b>{val} دقیقه</b> تنظیم شد.", parse_mode="html")
|
||||
await event.reply(card, parse_mode="html", buttons=buttons)
|
||||
except ValueError:
|
||||
menu = await self.get_menu()
|
||||
await event.reply("❌ مقدار وارد شده باید یک عدد صحیح (به دقیقه) باشد.", buttons=menu)
|
||||
|
||||
# 8.3. Waiting for Website Auto Re-analysis Interval
|
||||
elif action == "wait_website_reintv":
|
||||
site_id = state.get("site_id")
|
||||
self.user_states.pop(event.sender_id, None)
|
||||
try:
|
||||
val = int(text.strip())
|
||||
await self.repo.update_source_website_reanalyze_hours(site_id, val)
|
||||
card, buttons = await self._render_website_config(site_id)
|
||||
await event.reply(f"✅ دوره تحلیل مجدد خودکار وبسایت روی هر <b>{val} ساعت</b> تنظیم شد.", parse_mode="html")
|
||||
await event.reply(card, parse_mode="html", buttons=buttons)
|
||||
except ValueError:
|
||||
menu = await self.get_menu()
|
||||
await event.reply("❌ مقدار وارد شده باید یک عدد صحیح (به ساعت) باشد.", buttons=menu)
|
||||
|
||||
# 9. Waiting for AI Model
|
||||
elif action == "wait_ai_model":
|
||||
self.user_states.pop(event.sender_id, None)
|
||||
@@ -2275,6 +2476,109 @@ class AdminBotService:
|
||||
await event.edit(card, parse_mode="html", buttons=buttons)
|
||||
await event.answer()
|
||||
|
||||
elif data == "list_web":
|
||||
text, buttons = await self._render_website_list()
|
||||
await event.edit(text, parse_mode="html", buttons=buttons or None)
|
||||
await event.answer()
|
||||
|
||||
elif data.startswith("web_view:"):
|
||||
site_id = int(data.split(":")[1])
|
||||
card, buttons = await self._render_website_config(site_id)
|
||||
await event.edit(card, parse_mode="html", buttons=buttons)
|
||||
await event.answer()
|
||||
|
||||
elif data == "add_web":
|
||||
self.user_states[event.sender_id] = {"action": "wait_website_input"}
|
||||
guide = (
|
||||
"🌐 <b>افزودن وبسایت مبدا جدید:</b>\n\n"
|
||||
"لطفاً نام وبسایت و آدرس URL آن را با یک فاصله یا در دو خط ارسال کنید:\n"
|
||||
"<i>(مثال: <code>دیجیاتو https://digiato.com</code>)</i>\n\n"
|
||||
"🤖 هوش مصنوعی بلافاصله وبسایت را بررسی کرده و اندپوینتهای دریافت خبر را کشف میکند."
|
||||
)
|
||||
await event.reply(guide, parse_mode="html", buttons=get_cancel_button())
|
||||
await event.answer()
|
||||
|
||||
elif data.startswith("web_fetch:"):
|
||||
site_id = int(data.split(":")[1])
|
||||
await event.answer("🔄 در حال دریافت آخرین پستها از وبسایت...")
|
||||
cnt = await self.website_collector.fetch_website_posts(site_id) if self.website_collector else 0
|
||||
card, buttons = await self._render_website_config(site_id)
|
||||
await event.edit(card, parse_mode="html", buttons=buttons)
|
||||
await event.answer(f"✅ تعداد {cnt} پست جدید دریافت و پردازش شد.", alert=True)
|
||||
|
||||
elif data.startswith("web_reanalyze:"):
|
||||
site_id = int(data.split(":")[1])
|
||||
await event.answer("🤖 در حال تحلیل مجدد ساختار وبسایت توسط هوش مصنوعی...")
|
||||
ok, msg = await self.website_collector.reanalyze_website(site_id) if self.website_collector else (False, "سرویس در دسترس نیست")
|
||||
card, buttons = await self._render_website_config(site_id)
|
||||
await event.edit(card, parse_mode="html", buttons=buttons)
|
||||
await event.answer("✅ تحلیل مجدد با موفقیت انجام شد." if ok else f"❌ خطا: {msg}", alert=True)
|
||||
|
||||
elif data.startswith("web_intv:"):
|
||||
site_id = int(data.split(":")[1])
|
||||
site = await self.repo.get_source_website_by_id(site_id)
|
||||
if not site:
|
||||
await event.answer("وبسایت یافت نشد.", alert=True)
|
||||
return
|
||||
self.user_states[event.sender_id] = {"action": "wait_website_intv", "site_id": site_id}
|
||||
await event.reply(
|
||||
f"⏱ <b>تنظیم فاصله بررسی وبسایت «{site.name}»:</b>\n\n"
|
||||
f"فاصله زمانی بررسی و استخراج اخبار (به دقیقه) را ارسال کنید:\n"
|
||||
f"<i>(مثال: <code>30</code> برای هر ۳۰ دقیقه)</i>",
|
||||
parse_mode="html",
|
||||
buttons=get_cancel_button()
|
||||
)
|
||||
await event.answer()
|
||||
|
||||
elif data.startswith("web_reintv:"):
|
||||
site_id = int(data.split(":")[1])
|
||||
site = await self.repo.get_source_website_by_id(site_id)
|
||||
if not site:
|
||||
await event.answer("وبسایت یافت نشد.", alert=True)
|
||||
return
|
||||
self.user_states[event.sender_id] = {"action": "wait_website_reintv", "site_id": site_id}
|
||||
await event.reply(
|
||||
f"🔄 <b>تنظیم دوره تحلیل مجدد هوشمند وبسایت «{site.name}»:</b>\n\n"
|
||||
f"فاصله تحلیل خودکار ساختار و کشف اندپوینتها توسط AI (به ساعت) را ارسال کنید:\n"
|
||||
f"<i>(مثال: <code>24</code> برای هر ۲۴ ساعت، یا <code>0</code> برای غیرفعالسازی)</i>",
|
||||
parse_mode="html",
|
||||
buttons=get_cancel_button()
|
||||
)
|
||||
await event.answer()
|
||||
|
||||
elif data.startswith("web_cat:"):
|
||||
site_id = int(data.split(":")[1])
|
||||
text, buttons = await self._render_website_category_menu(site_id)
|
||||
await event.edit(text, parse_mode="html", buttons=buttons)
|
||||
await event.answer()
|
||||
|
||||
elif data.startswith("web_set_cat:"):
|
||||
_, site_id_str, cat_id_str = data.split(":")
|
||||
site_id = int(site_id_str)
|
||||
cat_id = None if cat_id_str == "none" else int(cat_id_str)
|
||||
await self.repo.update_source_website_category(site_id, cat_id)
|
||||
card, buttons = await self._render_website_config(site_id)
|
||||
await event.edit(card, parse_mode="html", buttons=buttons)
|
||||
await event.answer("✅ دستهبندی وبسایت بهروز شد.")
|
||||
|
||||
elif data.startswith("del_web:"):
|
||||
site_id = int(data.split(":")[1])
|
||||
await self.repo.delete_source_website(site_id)
|
||||
text, buttons = await self._render_website_list()
|
||||
await event.edit(text, parse_mode="html", buttons=buttons or None)
|
||||
await event.answer("🗑 وبسایت مبدا با موفقیت حذف شد.", alert=True)
|
||||
|
||||
elif data.startswith("trg_order:"):
|
||||
target_id = int(data.split(":")[1])
|
||||
target = await self.repo.get_target_by_id(target_id)
|
||||
if target:
|
||||
curr_order = getattr(target, "dispatch_order", "order") or "order"
|
||||
new_order = "random" if curr_order == "order" else "order"
|
||||
await self.repo.update_target_dispatch_order(target_id, new_order)
|
||||
card, buttons = await self._render_target_config(target_id)
|
||||
await event.edit(card, parse_mode="html", buttons=buttons)
|
||||
await event.answer("🎲 شیوه ارسال روی تصادفی تنظیم شد." if new_order == "random" else "🟢 شیوه ارسال روی به ترتیب تنظیم شد.")
|
||||
|
||||
elif data.startswith("auto_src:"):
|
||||
target_id = int(data.split(":")[1])
|
||||
text, buttons = await self._render_auto_sources(target_id)
|
||||
|
||||
Reference in New Issue
Block a user