AI Update: شروع به پیاده سازی کن
This commit is contained in:
@@ -152,17 +152,35 @@ CREATE INDEX IF NOT EXISTS idx_source_websites_active ON source_websites(is_acti
|
|||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_ai_providers_active ON ai_providers(is_active);
|
CREATE INDEX IF NOT EXISTS idx_ai_providers_active ON ai_providers(is_active);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS admin_channels (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
channel_id BIGINT UNIQUE NOT NULL,
|
||||||
|
title VARCHAR(255),
|
||||||
|
username VARCHAR(255),
|
||||||
|
is_default BOOLEAN DEFAULT FALSE,
|
||||||
|
is_active BOOLEAN DEFAULT TRUE,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_admin_channels_active ON admin_channels(is_active);
|
||||||
|
|
||||||
-- Migration safety for existing tables
|
-- Migration safety for existing tables
|
||||||
ALTER TABLE ai_providers ADD COLUMN IF NOT EXISTS fallback_provider_id BIGINT REFERENCES ai_providers(id) ON DELETE SET NULL;
|
ALTER TABLE ai_providers ADD COLUMN IF NOT EXISTS fallback_provider_id BIGINT REFERENCES ai_providers(id) ON DELETE SET NULL;
|
||||||
ALTER TABLE ai_providers ADD COLUMN IF NOT EXISTS supports_vision BOOLEAN DEFAULT FALSE;
|
ALTER TABLE ai_providers ADD COLUMN IF NOT EXISTS supports_vision BOOLEAN DEFAULT FALSE;
|
||||||
ALTER TABLE sources ADD COLUMN IF NOT EXISTS category_id INT REFERENCES channel_categories(id) ON DELETE SET NULL;
|
ALTER TABLE sources ADD COLUMN IF NOT EXISTS category_id INT REFERENCES channel_categories(id) ON DELETE SET NULL;
|
||||||
ALTER TABLE targets ADD COLUMN IF NOT EXISTS category_id INT REFERENCES channel_categories(id) ON DELETE SET NULL;
|
ALTER TABLE targets ADD COLUMN IF NOT EXISTS category_id INT REFERENCES channel_categories(id) ON DELETE SET NULL;
|
||||||
|
ALTER TABLE sources ADD COLUMN IF NOT EXISTS admin_channel_id BIGINT;
|
||||||
|
ALTER TABLE source_websites ADD COLUMN IF NOT EXISTS admin_channel_id BIGINT;
|
||||||
|
ALTER TABLE posts ADD COLUMN IF NOT EXISTS review_channel_id BIGINT;
|
||||||
ALTER TABLE sources ADD COLUMN IF NOT EXISTS is_active BOOLEAN DEFAULT TRUE;
|
ALTER TABLE sources ADD COLUMN IF NOT EXISTS is_active BOOLEAN DEFAULT TRUE;
|
||||||
ALTER TABLE targets ADD COLUMN IF NOT EXISTS is_active BOOLEAN DEFAULT TRUE;
|
ALTER TABLE targets ADD COLUMN IF NOT EXISTS is_active BOOLEAN DEFAULT TRUE;
|
||||||
ALTER TABLE sources ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP;
|
ALTER TABLE sources ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP;
|
||||||
ALTER TABLE targets ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP;
|
ALTER TABLE targets ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP;
|
||||||
CREATE INDEX IF NOT EXISTS idx_sources_category ON sources(category_id);
|
CREATE INDEX IF NOT EXISTS idx_sources_category ON sources(category_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_targets_category ON targets(category_id);
|
CREATE INDEX IF NOT EXISTS idx_targets_category ON targets(category_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_sources_admin_channel ON sources(admin_channel_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_source_websites_admin_channel ON source_websites(admin_channel_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_posts_review_channel ON posts(review_channel_id);
|
||||||
ALTER TABLE targets ADD COLUMN IF NOT EXISTS personality TEXT DEFAULT '';
|
ALTER TABLE targets ADD COLUMN IF NOT EXISTS personality TEXT DEFAULT '';
|
||||||
ALTER TABLE targets ADD COLUMN IF NOT EXISTS custom_footer TEXT DEFAULT '';
|
ALTER TABLE targets ADD COLUMN IF NOT EXISTS custom_footer TEXT DEFAULT '';
|
||||||
ALTER TABLE targets ADD COLUMN IF NOT EXISTS sleep_start_hour INT DEFAULT 0;
|
ALTER TABLE targets ADD COLUMN IF NOT EXISTS sleep_start_hour INT DEFAULT 0;
|
||||||
|
|||||||
@@ -1,6 +1,16 @@
|
|||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import Optional, List, Dict, Any
|
from typing import Optional, List, Dict, Any
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AdminReviewChannel:
|
||||||
|
id: Optional[int]
|
||||||
|
channel_id: int
|
||||||
|
title: Optional[str]
|
||||||
|
username: Optional[str] = None
|
||||||
|
is_default: bool = False
|
||||||
|
is_active: bool = True
|
||||||
|
created_at: Optional[str] = None
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ChannelCategory:
|
class ChannelCategory:
|
||||||
id: Optional[int]
|
id: Optional[int]
|
||||||
@@ -16,6 +26,7 @@ class SourceChannel:
|
|||||||
username: Optional[str]
|
username: Optional[str]
|
||||||
title: Optional[str]
|
title: Optional[str]
|
||||||
category_id: Optional[int] = None
|
category_id: Optional[int] = None
|
||||||
|
admin_channel_id: Optional[int] = None
|
||||||
context_message_count: int = 0
|
context_message_count: int = 0
|
||||||
is_active: bool = True
|
is_active: bool = True
|
||||||
created_at: Optional[str] = None
|
created_at: Optional[str] = None
|
||||||
@@ -26,6 +37,7 @@ class SourceWebsite:
|
|||||||
name: str
|
name: str
|
||||||
url: str
|
url: str
|
||||||
category_id: Optional[int] = None
|
category_id: Optional[int] = None
|
||||||
|
admin_channel_id: Optional[int] = None
|
||||||
check_interval_min: int = 30
|
check_interval_min: int = 30
|
||||||
auto_reanalyze_hours: int = 24
|
auto_reanalyze_hours: int = 24
|
||||||
last_reanalyzed_at: Optional[str] = None
|
last_reanalyzed_at: Optional[str] = None
|
||||||
@@ -82,6 +94,7 @@ class Post:
|
|||||||
rejection_reason: Optional[str] = None
|
rejection_reason: Optional[str] = None
|
||||||
published_to: List[Dict[str, Any]] = field(default_factory=list)
|
published_to: List[Dict[str, Any]] = field(default_factory=list)
|
||||||
is_deleted: bool = False
|
is_deleted: bool = False
|
||||||
|
review_channel_id: Optional[int] = None
|
||||||
review_message_id: Optional[int] = None
|
review_message_id: Optional[int] = None
|
||||||
scheduled_at: Optional[str] = None
|
scheduled_at: Optional[str] = None
|
||||||
published_at: Optional[str] = None
|
published_at: Optional[str] = None
|
||||||
|
|||||||
+193
-3
@@ -1,11 +1,17 @@
|
|||||||
import json
|
import json
|
||||||
import asyncpg
|
import asyncpg
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import List, Optional, Dict, Any, Tuple
|
from db.models import (
|
||||||
from db.models import SourceChannel, SourceWebsite, TargetChannel, Post, Setting, AILog, AIProviderProfile, ChannelCategory
|
SourceChannel, SourceWebsite, TargetChannel, Post, Setting,
|
||||||
|
AILog, AIProviderProfile, ChannelCategory, AdminReviewChannel
|
||||||
|
)
|
||||||
from db.database import get_db_pool
|
from db.database import get_db_pool
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_admin_channel_row(row: asyncpg.Record) -> AdminReviewChannel:
|
||||||
|
return AdminReviewChannel(**dict(row))
|
||||||
|
|
||||||
|
|
||||||
def _parse_post_row(row: asyncpg.Record) -> Post:
|
def _parse_post_row(row: asyncpg.Record) -> Post:
|
||||||
data = dict(row)
|
data = dict(row)
|
||||||
if isinstance(data.get("published_to"), str):
|
if isinstance(data.get("published_to"), str):
|
||||||
@@ -566,9 +572,15 @@ class Repository:
|
|||||||
)
|
)
|
||||||
return [_parse_post_row(r) for r in rows]
|
return [_parse_post_row(r) for r in rows]
|
||||||
|
|
||||||
async def update_review_message_id(self, post_id: int, review_message_id: int) -> None:
|
async def update_review_message_id(self, post_id: int, review_message_id: Optional[int], review_channel_id: Optional[int] = None) -> None:
|
||||||
pool = await self._get_pool()
|
pool = await self._get_pool()
|
||||||
async with pool.acquire() as conn:
|
async with pool.acquire() as conn:
|
||||||
|
if review_channel_id is not None:
|
||||||
|
await conn.execute(
|
||||||
|
"UPDATE posts SET review_message_id = $1, review_channel_id = $2 WHERE id = $3;",
|
||||||
|
review_message_id, review_channel_id, post_id,
|
||||||
|
)
|
||||||
|
else:
|
||||||
await conn.execute(
|
await conn.execute(
|
||||||
"UPDATE posts SET review_message_id = $1 WHERE id = $2;",
|
"UPDATE posts SET review_message_id = $1 WHERE id = $2;",
|
||||||
review_message_id, post_id,
|
review_message_id, post_id,
|
||||||
@@ -1044,6 +1056,184 @@ class Repository:
|
|||||||
description="Global emergency operational pause"
|
description="Global emergency operational pause"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# --- Admin Review Channels ---
|
||||||
|
async def add_admin_channel(
|
||||||
|
self,
|
||||||
|
channel_id: int,
|
||||||
|
title: Optional[str] = None,
|
||||||
|
username: Optional[str] = None,
|
||||||
|
is_default: bool = False
|
||||||
|
) -> int:
|
||||||
|
pool = await self._get_pool()
|
||||||
|
async with pool.acquire() as conn:
|
||||||
|
async with conn.transaction():
|
||||||
|
if is_default:
|
||||||
|
await conn.execute("UPDATE admin_channels SET is_default = FALSE;")
|
||||||
|
row = await conn.fetchrow(
|
||||||
|
"""
|
||||||
|
INSERT INTO admin_channels (channel_id, title, username, is_default, is_active)
|
||||||
|
VALUES ($1, $2, $3, $4, TRUE)
|
||||||
|
ON CONFLICT(channel_id) DO UPDATE SET
|
||||||
|
title = COALESCE(EXCLUDED.title, admin_channels.title),
|
||||||
|
username = COALESCE(EXCLUDED.username, admin_channels.username),
|
||||||
|
is_default = EXCLUDED.is_default,
|
||||||
|
is_active = TRUE
|
||||||
|
RETURNING id;
|
||||||
|
""",
|
||||||
|
channel_id, title, username, is_default
|
||||||
|
)
|
||||||
|
return row["id"]
|
||||||
|
|
||||||
|
async def get_admin_channels(self) -> List[AdminReviewChannel]:
|
||||||
|
pool = await self._get_pool()
|
||||||
|
async with pool.acquire() as conn:
|
||||||
|
rows = await conn.fetch("SELECT * FROM admin_channels WHERE is_active = TRUE ORDER BY is_default DESC, id ASC;")
|
||||||
|
return [_parse_admin_channel_row(r) for r in rows]
|
||||||
|
|
||||||
|
async def get_admin_channel_by_id(self, admin_id: int) -> Optional[AdminReviewChannel]:
|
||||||
|
pool = await self._get_pool()
|
||||||
|
async with pool.acquire() as conn:
|
||||||
|
row = await conn.fetchrow("SELECT * FROM admin_channels WHERE id = $1;", admin_id)
|
||||||
|
return _parse_admin_channel_row(row) if row else None
|
||||||
|
|
||||||
|
async def get_admin_channel_by_telegram_id(self, channel_id: int) -> Optional[AdminReviewChannel]:
|
||||||
|
pool = await self._get_pool()
|
||||||
|
async with pool.acquire() as conn:
|
||||||
|
row = await conn.fetchrow("SELECT * FROM admin_channels WHERE channel_id = $1;", channel_id)
|
||||||
|
return _parse_admin_channel_row(row) if row else None
|
||||||
|
|
||||||
|
async def get_default_admin_channel(self) -> Optional[AdminReviewChannel]:
|
||||||
|
pool = await self._get_pool()
|
||||||
|
async with pool.acquire() as conn:
|
||||||
|
row = await conn.fetchrow("SELECT * FROM admin_channels WHERE is_active = TRUE AND is_default = TRUE LIMIT 1;")
|
||||||
|
if not row:
|
||||||
|
row = await conn.fetchrow("SELECT * FROM admin_channels WHERE is_active = TRUE ORDER BY id ASC LIMIT 1;")
|
||||||
|
return _parse_admin_channel_row(row) if row else None
|
||||||
|
|
||||||
|
async def set_default_admin_channel(self, admin_id: int) -> None:
|
||||||
|
pool = await self._get_pool()
|
||||||
|
async with pool.acquire() as conn:
|
||||||
|
async with conn.transaction():
|
||||||
|
await conn.execute("UPDATE admin_channels SET is_default = FALSE;")
|
||||||
|
await conn.execute("UPDATE admin_channels SET is_default = TRUE WHERE id = $1;", admin_id)
|
||||||
|
|
||||||
|
async def delete_admin_channel(self, admin_id: int) -> None:
|
||||||
|
pool = await self._get_pool()
|
||||||
|
async with pool.acquire() as conn:
|
||||||
|
row = await conn.fetchrow("SELECT channel_id FROM admin_channels WHERE id = $1;", admin_id)
|
||||||
|
if row:
|
||||||
|
ch_id = row["channel_id"]
|
||||||
|
await conn.execute("UPDATE sources SET admin_channel_id = NULL WHERE admin_channel_id = $1;", ch_id)
|
||||||
|
await conn.execute("UPDATE source_websites SET admin_channel_id = NULL WHERE admin_channel_id = $1;", ch_id)
|
||||||
|
await conn.execute("DELETE FROM admin_channels WHERE id = $1;", admin_id)
|
||||||
|
|
||||||
|
# --- Source & Website Admin Channel Setters ---
|
||||||
|
async def set_source_admin_channel(self, source_id: int, admin_channel_id: Optional[int]) -> None:
|
||||||
|
pool = await self._get_pool()
|
||||||
|
async with pool.acquire() as conn:
|
||||||
|
await conn.execute("UPDATE sources SET admin_channel_id = $1 WHERE id = $2;", admin_channel_id, source_id)
|
||||||
|
|
||||||
|
async def set_website_admin_channel(self, site_id: int, admin_channel_id: Optional[int]) -> None:
|
||||||
|
pool = await self._get_pool()
|
||||||
|
async with pool.acquire() as conn:
|
||||||
|
await conn.execute("UPDATE source_websites SET admin_channel_id = $1 WHERE id = $2;", admin_channel_id, site_id)
|
||||||
|
|
||||||
|
# --- Review Purge Helpers ---
|
||||||
|
async def get_review_posts_by_admin_channel(self, admin_channel_id: int) -> List[Post]:
|
||||||
|
pool = await self._get_pool()
|
||||||
|
async with pool.acquire() as conn:
|
||||||
|
rows = await conn.fetch(
|
||||||
|
"""
|
||||||
|
SELECT * FROM posts
|
||||||
|
WHERE review_channel_id = $1
|
||||||
|
AND review_message_id IS NOT NULL
|
||||||
|
AND is_deleted = FALSE
|
||||||
|
ORDER BY id ASC;
|
||||||
|
""",
|
||||||
|
admin_channel_id
|
||||||
|
)
|
||||||
|
return [_parse_post_row(r) for r in rows]
|
||||||
|
|
||||||
|
async def count_review_posts_by_admin_channel(self, admin_channel_id: int) -> int:
|
||||||
|
pool = await self._get_pool()
|
||||||
|
async with pool.acquire() as conn:
|
||||||
|
val = await conn.fetchval(
|
||||||
|
"""
|
||||||
|
SELECT COUNT(*) FROM posts
|
||||||
|
WHERE review_channel_id = $1
|
||||||
|
AND review_message_id IS NOT NULL
|
||||||
|
AND is_deleted = FALSE;
|
||||||
|
""",
|
||||||
|
admin_channel_id
|
||||||
|
)
|
||||||
|
return val or 0
|
||||||
|
|
||||||
|
async def clear_review_messages_for_channel(self, admin_channel_id: int) -> List[Post]:
|
||||||
|
pool = await self._get_pool()
|
||||||
|
async with pool.acquire() as conn:
|
||||||
|
async with conn.transaction():
|
||||||
|
rows = await conn.fetch(
|
||||||
|
"""
|
||||||
|
SELECT * FROM posts
|
||||||
|
WHERE review_channel_id = $1
|
||||||
|
AND review_message_id IS NOT NULL
|
||||||
|
AND is_deleted = FALSE;
|
||||||
|
""",
|
||||||
|
admin_channel_id
|
||||||
|
)
|
||||||
|
posts = [_parse_post_row(r) for r in rows]
|
||||||
|
await conn.execute(
|
||||||
|
"""
|
||||||
|
UPDATE posts
|
||||||
|
SET review_message_id = NULL,
|
||||||
|
is_deleted = TRUE,
|
||||||
|
status = 'deleted'
|
||||||
|
WHERE review_channel_id = $1
|
||||||
|
AND review_message_id IS NOT NULL
|
||||||
|
AND is_deleted = FALSE;
|
||||||
|
""",
|
||||||
|
admin_channel_id
|
||||||
|
)
|
||||||
|
return posts
|
||||||
|
|
||||||
|
async def get_all_review_posts_with_message_id(self) -> List[Post]:
|
||||||
|
pool = await self._get_pool()
|
||||||
|
async with pool.acquire() as conn:
|
||||||
|
rows = await conn.fetch(
|
||||||
|
"""
|
||||||
|
SELECT * FROM posts
|
||||||
|
WHERE review_message_id IS NOT NULL
|
||||||
|
AND is_deleted = FALSE
|
||||||
|
ORDER BY id ASC;
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
return [_parse_post_row(r) for r in rows]
|
||||||
|
|
||||||
|
async def clear_all_review_messages(self) -> List[Post]:
|
||||||
|
pool = await self._get_pool()
|
||||||
|
async with pool.acquire() as conn:
|
||||||
|
async with conn.transaction():
|
||||||
|
rows = await conn.fetch(
|
||||||
|
"""
|
||||||
|
SELECT * FROM posts
|
||||||
|
WHERE review_message_id IS NOT NULL
|
||||||
|
AND is_deleted = FALSE;
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
posts = [_parse_post_row(r) for r in rows]
|
||||||
|
await conn.execute(
|
||||||
|
"""
|
||||||
|
UPDATE posts
|
||||||
|
SET review_message_id = NULL,
|
||||||
|
is_deleted = TRUE,
|
||||||
|
status = 'deleted'
|
||||||
|
WHERE review_message_id IS NOT NULL
|
||||||
|
AND is_deleted = FALSE;
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
return posts
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+420
-16
@@ -69,7 +69,8 @@ def get_source_fetch_buttons(channel_id: int, source_id: Optional[int] = None):
|
|||||||
]
|
]
|
||||||
if source_id is not None:
|
if source_id is not None:
|
||||||
rows.append([
|
rows.append([
|
||||||
Button.inline("📜 تنظیم پیامهای زمینه (Context)", data=f"src_ctx_menu:{source_id}"),
|
Button.inline("📜 پیامهای زمینه (Context)", data=f"src_ctx_menu:{source_id}"),
|
||||||
|
Button.inline("📋 کانال نظارت ادمین", data=f"src_adm:{source_id}"),
|
||||||
])
|
])
|
||||||
rows.append([
|
rows.append([
|
||||||
Button.inline("📁 تعیین دستهبندی", data=f"src_cat:{source_id}"),
|
Button.inline("📁 تعیین دستهبندی", data=f"src_cat:{source_id}"),
|
||||||
@@ -663,9 +664,31 @@ class AdminBotService:
|
|||||||
)
|
)
|
||||||
return caption
|
return caption
|
||||||
|
|
||||||
|
async def get_effective_review_channel_id(self, post: Post) -> Optional[int]:
|
||||||
|
"""Determine which Admin Review Channel this post should be sent to."""
|
||||||
|
if post.source_channel_id <= -900000000:
|
||||||
|
site_id = abs(post.source_channel_id + 900000000)
|
||||||
|
site = await self.repo.get_source_website_by_id(site_id)
|
||||||
|
if site and site.admin_channel_id:
|
||||||
|
return site.admin_channel_id
|
||||||
|
else:
|
||||||
|
src = await self.repo.get_source_by_channel_id(post.source_channel_id)
|
||||||
|
if src and src.admin_channel_id:
|
||||||
|
return src.admin_channel_id
|
||||||
|
|
||||||
|
default_adm = await self.repo.get_default_admin_channel()
|
||||||
|
if default_adm and default_adm.channel_id:
|
||||||
|
return default_adm.channel_id
|
||||||
|
|
||||||
|
return self.review_channel_id or None
|
||||||
|
|
||||||
async def send_raw_review_post(self, post_id: int, refresh_only: bool = False):
|
async def send_raw_review_post(self, post_id: int, refresh_only: bool = False):
|
||||||
post = await self.repo.get_post_by_id(post_id)
|
post = await self.repo.get_post_by_id(post_id)
|
||||||
if not post or not self.review_channel_id or post.is_deleted:
|
if not post or post.is_deleted:
|
||||||
|
return
|
||||||
|
|
||||||
|
target_review_channel_id = post.review_channel_id or await self.get_effective_review_channel_id(post)
|
||||||
|
if not target_review_channel_id:
|
||||||
return
|
return
|
||||||
|
|
||||||
targets = await self.repo.get_active_targets()
|
targets = await self.repo.get_active_targets()
|
||||||
@@ -678,7 +701,7 @@ class AdminBotService:
|
|||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
await self.client.edit_message(
|
await self.client.edit_message(
|
||||||
self.review_channel_id, post.review_message_id,
|
target_review_channel_id, post.review_message_id,
|
||||||
clamp_for_telegram(caption, bool(post.media_path)),
|
clamp_for_telegram(caption, bool(post.media_path)),
|
||||||
parse_mode="html", buttons=keyboard,
|
parse_mode="html", buttons=keyboard,
|
||||||
)
|
)
|
||||||
@@ -692,7 +715,7 @@ class AdminBotService:
|
|||||||
try:
|
try:
|
||||||
if has_media:
|
if has_media:
|
||||||
msg = await self.client.send_file(
|
msg = await self.client.send_file(
|
||||||
self.review_channel_id,
|
target_review_channel_id,
|
||||||
file=post.media_path,
|
file=post.media_path,
|
||||||
caption=caption,
|
caption=caption,
|
||||||
parse_mode="html",
|
parse_mode="html",
|
||||||
@@ -700,14 +723,14 @@ class AdminBotService:
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
msg = await self.client.send_message(
|
msg = await self.client.send_message(
|
||||||
self.review_channel_id,
|
target_review_channel_id,
|
||||||
caption,
|
caption,
|
||||||
parse_mode="html",
|
parse_mode="html",
|
||||||
buttons=keyboard
|
buttons=keyboard
|
||||||
)
|
)
|
||||||
await self.repo.update_review_message_id(post.id, msg.id)
|
await self.repo.update_review_message_id(post.id, msg.id, review_channel_id=target_review_channel_id)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
await log_exception("admin_bot.send_review_post", e, {"post_id": post.id, "review_channel_id": self.review_channel_id})
|
await log_exception("admin_bot.send_review_post", e, {"post_id": post.id, "review_channel_id": target_review_channel_id})
|
||||||
|
|
||||||
async def refresh_error_metrics(self) -> int:
|
async def refresh_error_metrics(self) -> int:
|
||||||
"""Republish the open-error gauges from the database. Returns the open count."""
|
"""Republish the open-error gauges from the database. Returns the open count."""
|
||||||
@@ -954,6 +977,14 @@ class AdminBotService:
|
|||||||
if cat:
|
if cat:
|
||||||
cat_name = cat.name
|
cat_name = cat.name
|
||||||
|
|
||||||
|
adm_name = "پیشفرض سیستم"
|
||||||
|
if getattr(source, "admin_channel_id", None):
|
||||||
|
adm = await self.repo.get_admin_channel_by_telegram_id(source.admin_channel_id)
|
||||||
|
if adm:
|
||||||
|
adm_name = f"<b>{adm.title or adm.channel_id}</b>"
|
||||||
|
else:
|
||||||
|
adm_name = f"<code>{source.admin_channel_id}</code>"
|
||||||
|
|
||||||
auto_targets = await self.repo.get_targets_auto_routed_from(source.channel_id)
|
auto_targets = await self.repo.get_targets_auto_routed_from(source.channel_id)
|
||||||
auto_line = (
|
auto_line = (
|
||||||
"🤖 <b>ارسال خودکار به:</b> " + "، ".join(f"<b>{t.title}</b>" for t in auto_targets)
|
"🤖 <b>ارسال خودکار به:</b> " + "، ".join(f"<b>{t.title}</b>" for t in auto_targets)
|
||||||
@@ -968,6 +999,7 @@ class AdminBotService:
|
|||||||
f"• 🆔 <b>شناسه کانال:</b> <code>{source.channel_id}</code>\n"
|
f"• 🆔 <b>شناسه کانال:</b> <code>{source.channel_id}</code>\n"
|
||||||
f"• 🔗 <b>یوزرنیم:</b> @{source.username or 'ندارد'}\n"
|
f"• 🔗 <b>یوزرنیم:</b> @{source.username or 'ندارد'}\n"
|
||||||
f"• 📁 <b>دستهبندی:</b> <b>{cat_name}</b>\n"
|
f"• 📁 <b>دستهبندی:</b> <b>{cat_name}</b>\n"
|
||||||
|
f"• 📋 <b>کانال نظارت ادمین:</b> {adm_name}\n"
|
||||||
f"• 📜 <b>پیامهای زمینه (Context):</b> <b>{ctx_label}</b>\n"
|
f"• 📜 <b>پیامهای زمینه (Context):</b> <b>{ctx_label}</b>\n"
|
||||||
f"• 📥 <b>پستهای دریافتشده:</b> <b>{collected}</b>\n"
|
f"• 📥 <b>پستهای دریافتشده:</b> <b>{collected}</b>\n"
|
||||||
f"• {auto_line}\n\n"
|
f"• {auto_line}\n\n"
|
||||||
@@ -1053,12 +1085,21 @@ class AdminBotService:
|
|||||||
last_re = site.last_reanalyzed_at.strftime("%Y-%m-%d %H:%M") if site.last_reanalyzed_at else "انجام نشده"
|
last_re = site.last_reanalyzed_at.strftime("%Y-%m-%d %H:%M") if site.last_reanalyzed_at else "انجام نشده"
|
||||||
needs = getattr(site, "custom_instructions", "") or ""
|
needs = getattr(site, "custom_instructions", "") or ""
|
||||||
|
|
||||||
|
adm_name = "پیشفرض سیستم"
|
||||||
|
if getattr(site, "admin_channel_id", None):
|
||||||
|
adm = await self.repo.get_admin_channel_by_telegram_id(site.admin_channel_id)
|
||||||
|
if adm:
|
||||||
|
adm_name = f"<b>{adm.title or adm.channel_id}</b>"
|
||||||
|
else:
|
||||||
|
adm_name = f"<code>{site.admin_channel_id}</code>"
|
||||||
|
|
||||||
text = (
|
text = (
|
||||||
f"🌐 <b>وبسایت مبدا:</b> <b>{site.name}</b>\n\n"
|
f"🌐 <b>وبسایت مبدا:</b> <b>{site.name}</b>\n\n"
|
||||||
f"• 🔗 <b>آدرس وبسایت:</b> {site.url}\n"
|
f"• 🔗 <b>آدرس وبسایت:</b> {site.url}\n"
|
||||||
f"• 🔌 <b>اندپوینت دریافت:</b> <code>{endpoint}</code>\n"
|
f"• 🔌 <b>اندپوینت دریافت:</b> <code>{endpoint}</code>\n"
|
||||||
f"• 🏷 <b>نوع ساختار:</b> <code>{parser_type}</code>\n"
|
f"• 🏷 <b>نوع ساختار:</b> <code>{parser_type}</code>\n"
|
||||||
f"• 📁 <b>دستهبندی:</b> <b>{cat_name}</b>\n"
|
f"• 📁 <b>دستهبندی:</b> <b>{cat_name}</b>\n"
|
||||||
|
f"• 📋 <b>کانال نظارت ادمین:</b> {adm_name}\n"
|
||||||
f"• 📝 <b>نیازمندیها و فیلترهای استخراج:</b>\n"
|
f"• 📝 <b>نیازمندیها و فیلترهای استخراج:</b>\n"
|
||||||
f"<i>{needs or 'ثبت نشده (استخراج تمام اخبار و پستها)'}</i>\n\n"
|
f"<i>{needs or 'ثبت نشده (استخراج تمام اخبار و پستها)'}</i>\n\n"
|
||||||
f"• ⏱ <b>فاصله بررسی:</b> هر <b>{site.check_interval_min} دقیقه</b>\n"
|
f"• ⏱ <b>فاصله بررسی:</b> هر <b>{site.check_interval_min} دقیقه</b>\n"
|
||||||
@@ -1078,11 +1119,14 @@ class AdminBotService:
|
|||||||
Button.inline("⏱ تغییر فاصله بررسی", data=f"web_intv:{site.id}")
|
Button.inline("⏱ تغییر فاصله بررسی", data=f"web_intv:{site.id}")
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
Button.inline("🔄 دوره تحلیل مجدد AI", data=f"web_reintv:{site.id}"),
|
Button.inline("📋 کانال نظارت ادمین", data=f"web_adm:{site.id}"),
|
||||||
Button.inline("📁 تعیین دستهبندی", data=f"web_cat:{site.id}")
|
Button.inline("📁 تعیین دستهبندی", data=f"web_cat:{site.id}")
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
Button.inline("🗑 حذف این وبسایت", data=f"del_web:{site.id}"),
|
Button.inline("🔄 دوره تحلیل مجدد AI", data=f"web_reintv:{site.id}"),
|
||||||
|
Button.inline("🗑 حذف این وبسایت", data=f"del_web:{site.id}")
|
||||||
|
],
|
||||||
|
[
|
||||||
Button.inline("🔙 بازگشت به لیست وبسایتها", data="list_web")
|
Button.inline("🔙 بازگشت به لیست وبسایتها", data="list_web")
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
@@ -1144,7 +1188,10 @@ class AdminBotService:
|
|||||||
Button.inline("➕ افزودن مبدا / مقصد جدید", data="hub_add"),
|
Button.inline("➕ افزودن مبدا / مقصد جدید", data="hub_add"),
|
||||||
Button.inline("📂 دستهبندی موضوعی کانالها", data="hub_cats")
|
Button.inline("📂 دستهبندی موضوعی کانالها", data="hub_cats")
|
||||||
],
|
],
|
||||||
[Button.inline("🔙 بازگشت به منوی اصلی", data="back_to_main")]
|
[
|
||||||
|
Button.inline("🗑 پاکسازی پیامهای کانالهای ادمین", data="purge_adm_menu"),
|
||||||
|
Button.inline("🔙 بازگشت به منوی اصلی", data="back_to_main")
|
||||||
|
]
|
||||||
]
|
]
|
||||||
return text, buttons
|
return text, buttons
|
||||||
|
|
||||||
@@ -1152,11 +1199,13 @@ class AdminBotService:
|
|||||||
sources = await self.repo.get_active_sources()
|
sources = await self.repo.get_active_sources()
|
||||||
targets = await self.repo.get_active_targets()
|
targets = await self.repo.get_active_targets()
|
||||||
websites = await self.repo.get_active_source_websites()
|
websites = await self.repo.get_active_source_websites()
|
||||||
|
adm_channels = await self.repo.get_admin_channels()
|
||||||
text = (
|
text = (
|
||||||
f"🤖 <b>بخش کانالها، وبسایتها و رباتها (Bots Hub):</b>\n\n"
|
f"🤖 <b>بخش کانالها، وبسایتها و رباتها (Bots Hub):</b>\n\n"
|
||||||
f"• 📡 <b>کانالهای مبدا:</b> {len(sources)} کانال\n"
|
f"• 📡 <b>کانالهای مبدا:</b> {len(sources)} کانال\n"
|
||||||
f"• 🎯 <b>کانالهای مقصد:</b> {len(targets)} کانال\n"
|
f"• 🎯 <b>کانالهای مقصد:</b> {len(targets)} کانال\n"
|
||||||
f"• 🌐 <b>وبسایتهای مبدا:</b> {len(websites)} وبسایت\n\n"
|
f"• 🌐 <b>وبسایتهای مبدا:</b> {len(websites)} وبسایت\n"
|
||||||
|
f"• 📋 <b>کانالهای نظارت ادمین:</b> {len(adm_channels)} کانال ثبتشده\n\n"
|
||||||
f"<i>بخش مورد نظر را برای مشاهده و مدیریت انتخاب کنید:</i>"
|
f"<i>بخش مورد نظر را برای مشاهده و مدیریت انتخاب کنید:</i>"
|
||||||
)
|
)
|
||||||
buttons = [
|
buttons = [
|
||||||
@@ -1173,10 +1222,13 @@ class AdminBotService:
|
|||||||
Button.inline("➕ افزودن وبسایت", data="add_web")
|
Button.inline("➕ افزودن وبسایت", data="add_web")
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
Button.inline("🔑 احراز هویت یوزربات", data="hub_req_code"),
|
Button.inline("📋 کانالهای نظارت ادمین", data="list_adm_channels"),
|
||||||
Button.inline("📂 مدیریت دستهبندیها", data="list_cat")
|
Button.inline("📂 مدیریت دستهبندیها", data="list_cat")
|
||||||
],
|
],
|
||||||
[Button.inline("🔙 بازگشت به منوی اصلی", data="back_to_main")]
|
[
|
||||||
|
Button.inline("🔑 احراز هویت یوزربات", data="hub_req_code"),
|
||||||
|
Button.inline("🔙 بازگشت به منوی اصلی", data="back_to_main")
|
||||||
|
]
|
||||||
]
|
]
|
||||||
return text, buttons
|
return text, buttons
|
||||||
|
|
||||||
@@ -1194,13 +1246,198 @@ class AdminBotService:
|
|||||||
buttons = [
|
buttons = [
|
||||||
[Button.inline(pause_btn_text, data=pause_cb)],
|
[Button.inline(pause_btn_text, data=pause_cb)],
|
||||||
[
|
[
|
||||||
Button.inline("📝 گزارش تغییرات (Release Notes)", data="sys_changes"),
|
Button.inline("🗑 پاکسازی پیامهای کانالهای ادمین", data="purge_adm_menu"),
|
||||||
Button.inline("❓ راهنمای سیستم", data="sys_help")
|
Button.inline("📝 گزارش تغییرات", data="sys_changes")
|
||||||
],
|
],
|
||||||
[Button.inline("🔙 بازگشت به منوی اصلی", data="back_to_main")]
|
[
|
||||||
|
Button.inline("❓ راهنمای سیستم", data="sys_help"),
|
||||||
|
Button.inline("🔙 بازگشت به منوی اصلی", data="back_to_main")
|
||||||
|
]
|
||||||
]
|
]
|
||||||
return text, buttons
|
return text, buttons
|
||||||
|
|
||||||
|
async def _render_source_admin_channel_menu(self, source_id: int) -> Tuple[str, List[List[Button]]]:
|
||||||
|
source = await self.repo.get_source_by_id(source_id)
|
||||||
|
if not source:
|
||||||
|
return "❌ کانال مبدا یافت نشد.", []
|
||||||
|
|
||||||
|
adm_channels = await self.repo.get_admin_channels()
|
||||||
|
text = (
|
||||||
|
f"📋 <b>تعیین کانال نظارت ادمین برای کانال مبدا:</b>\n"
|
||||||
|
f"📢 <b>{source.title or source.channel_id}</b>\n\n"
|
||||||
|
f"پستهای جدید این کانال مبدا برای بررسی به کدام کانال مدیر/نظارت ارسال شوند؟\n\n"
|
||||||
|
f"<i>یکی از کانالهای زیر را انتخاب کنید یا گزینه پیشفرض را بزنید:</i>"
|
||||||
|
)
|
||||||
|
buttons = []
|
||||||
|
for a in adm_channels:
|
||||||
|
mark = "✅ " if source.admin_channel_id == a.channel_id else ""
|
||||||
|
def_tag = " (پیشفرض)" if a.is_default else ""
|
||||||
|
buttons.append([Button.inline(f"{mark}📋 {a.title or a.channel_id}{def_tag}", data=f"src_set_adm:{source.id}:{a.channel_id}")])
|
||||||
|
|
||||||
|
is_def = (source.admin_channel_id is None)
|
||||||
|
def_mark = "✅ " if is_def else ""
|
||||||
|
buttons.append([Button.inline(f"{def_mark}⚙️ پیشفرض سیستم ({self.review_channel_id})", data=f"src_set_adm:{source.id}:none")])
|
||||||
|
buttons.append([Button.inline("➕ افزودن کانال ادمین جدید", data=f"src_add_adm:{source.id}")])
|
||||||
|
buttons.append([Button.inline("🔙 بازگشت به تنظیمات کانال مبدا", data=f"src_view:{source.id}")])
|
||||||
|
return text, buttons
|
||||||
|
|
||||||
|
async def _render_website_admin_channel_menu(self, site_id: int) -> Tuple[str, List[List[Button]]]:
|
||||||
|
site = await self.repo.get_source_website_by_id(site_id)
|
||||||
|
if not site:
|
||||||
|
return "❌ وبسایت یافت نشد.", []
|
||||||
|
|
||||||
|
adm_channels = await self.repo.get_admin_channels()
|
||||||
|
text = (
|
||||||
|
f"📋 <b>تعیین کانال نظارت ادمین برای وبسایت مبدا:</b>\n"
|
||||||
|
f"🌐 <b>{site.name}</b>\n\n"
|
||||||
|
f"پستهای استخراجشده از این وبسایت برای بررسی به کدام کانال مدیر/نظارت ارسال شوند؟\n\n"
|
||||||
|
f"<i>یکی از کانالهای زیر را انتخاب کنید یا گزینه پیشفرض را بزنید:</i>"
|
||||||
|
)
|
||||||
|
buttons = []
|
||||||
|
for a in adm_channels:
|
||||||
|
mark = "✅ " if site.admin_channel_id == a.channel_id else ""
|
||||||
|
def_tag = " (پیشفرض)" if a.is_default else ""
|
||||||
|
buttons.append([Button.inline(f"{mark}📋 {a.title or a.channel_id}{def_tag}", data=f"web_set_adm:{site.id}:{a.channel_id}")])
|
||||||
|
|
||||||
|
is_def = (site.admin_channel_id is None)
|
||||||
|
def_mark = "✅ " if is_def else ""
|
||||||
|
buttons.append([Button.inline(f"{def_mark}⚙️ پیشفرض سیستم ({self.review_channel_id})", data=f"web_set_adm:{site.id}:none")])
|
||||||
|
buttons.append([Button.inline("➕ افزودن کانال ادمین جدید", data=f"web_add_adm:{site.id}")])
|
||||||
|
buttons.append([Button.inline("🔙 بازگشت به تنظیمات وبسایت", data=f"web_view:{site.id}")])
|
||||||
|
return text, buttons
|
||||||
|
|
||||||
|
async def _render_admin_channels_menu(self) -> Tuple[str, List[List[Button]]]:
|
||||||
|
channels = await self.repo.get_admin_channels()
|
||||||
|
lines = [
|
||||||
|
"📋 <b>مدیریت کانالهای نظارت و بازبینی ادمین (Admin Review Channels)</b>\n",
|
||||||
|
"شما میتوانید چندین کانال ادمین مجزا تعریف کنید تا پستهای منابع مختلف به کانالهای مدیر متفاوتی هدایت شوند:\n"
|
||||||
|
]
|
||||||
|
buttons = []
|
||||||
|
if channels:
|
||||||
|
for ch in channels:
|
||||||
|
def_star = " ⭐️ (پیشفرض)" if ch.is_default else ""
|
||||||
|
lines.append(f"• <b>{ch.title or 'کانال ادمین'}</b> (<code>{ch.channel_id}</code>){def_star}")
|
||||||
|
buttons.append([
|
||||||
|
Button.inline(f"📋 {ch.title or ch.channel_id}{def_star}", data=f"adm_view:{ch.id}"),
|
||||||
|
])
|
||||||
|
else:
|
||||||
|
lines.append(f"<i>تنها کانال پیشفرض محیطی فعال است: <code>{self.review_channel_id}</code></i>")
|
||||||
|
|
||||||
|
buttons.append([Button.inline("➕ افزودن کانال ادمین جدید", data="add_adm_ch")])
|
||||||
|
buttons.append([Button.inline("🔙 بازگشت به منوی رباتها", data="hub_bots")])
|
||||||
|
return "\n".join(lines), buttons
|
||||||
|
|
||||||
|
async def _render_admin_channel_detail(self, admin_id: int) -> Tuple[str, List[List[Button]]]:
|
||||||
|
adm = await self.repo.get_admin_channel_by_id(admin_id)
|
||||||
|
if not adm:
|
||||||
|
return "❌ کانال ادمین یافت نشد.", [[Button.inline("🔙 بازگشت", data="list_adm_channels")]]
|
||||||
|
|
||||||
|
pending_count = await self.repo.count_review_posts_by_admin_channel(adm.channel_id)
|
||||||
|
def_line = "⭐️ <b>وضعیت:</b> کانال پیشفرض سیستم" if adm.is_default else "⚪️ <b>وضعیت:</b> کانال فرعی"
|
||||||
|
|
||||||
|
text = (
|
||||||
|
f"📋 <b>مشخصات کانال نظارت ادمین:</b> <b>{adm.title or adm.channel_id}</b>\n\n"
|
||||||
|
f"• 🆔 <b>شناسه عددی:</b> <code>{adm.channel_id}</code>\n"
|
||||||
|
f"• 🔗 <b>یوزرنیم:</b> @{adm.username or 'ندارد'}\n"
|
||||||
|
f"• {def_line}\n"
|
||||||
|
f"• 📨 <b>پیامهای بررسینشده در این کانال:</b> <b>{pending_count}</b> پیام\n\n"
|
||||||
|
f"<i>عملیات مورد نظر را انتخاب کنید:</i>"
|
||||||
|
)
|
||||||
|
buttons = []
|
||||||
|
if not adm.is_default:
|
||||||
|
buttons.append([Button.inline("⭐️ تعیین بهعنوان پیشفرض سیستم", data=f"adm_set_def:{adm.id}")])
|
||||||
|
buttons.append([Button.inline(f"🗑 پاکسازی پیامهای این کانال ({pending_count})", data=f"purge_adm_ask:{adm.channel_id}")])
|
||||||
|
buttons.append([Button.inline("❌ حذف این کانال ادمین", data=f"adm_del:{adm.id}")])
|
||||||
|
buttons.append([Button.inline("🔙 بازگشت به لیست کانالهای ادمین", data="list_adm_channels")])
|
||||||
|
return text, buttons
|
||||||
|
|
||||||
|
async def _render_purge_admin_channels_menu(self) -> Tuple[str, List[List[Button]]]:
|
||||||
|
adm_channels = await self.repo.get_admin_channels()
|
||||||
|
all_channels_map: Dict[int, str] = {}
|
||||||
|
if self.review_channel_id:
|
||||||
|
all_channels_map[self.review_channel_id] = "کانال پیشفرض محیطی"
|
||||||
|
|
||||||
|
for a in adm_channels:
|
||||||
|
all_channels_map[a.channel_id] = a.title or str(a.channel_id)
|
||||||
|
|
||||||
|
lines = [
|
||||||
|
"🗑 <b>پاکسازی پیامهای کانالهای ادمین (Purge Review Messages)</b>\n",
|
||||||
|
"با استفاده از این بخش میتوانید با یک دکمه، تمام کارتها و پیامهای ارسالشده به کانالهای مدیران را بهصورت تفکیکشده یا یکجا پاکسازی کنید:\n"
|
||||||
|
]
|
||||||
|
buttons = []
|
||||||
|
total_pending = 0
|
||||||
|
|
||||||
|
for ch_id, title in all_channels_map.items():
|
||||||
|
cnt = await self.repo.count_review_posts_by_admin_channel(ch_id)
|
||||||
|
total_pending += cnt
|
||||||
|
lines.append(f"• 📋 <b>{title}</b> (<code>{ch_id}</code>): <b>{cnt}</b> پیام بررسینشده")
|
||||||
|
buttons.append([
|
||||||
|
Button.inline(f"🗑 پاکسازی پیامهای {title[:20]} ({cnt})", data=f"purge_adm_ask:{ch_id}")
|
||||||
|
])
|
||||||
|
|
||||||
|
if total_pending > 0 or len(all_channels_map) > 1:
|
||||||
|
buttons.append([Button.inline(f"🔥 پاکسازی همه کانالهای ادمین (مجموع {total_pending})", data="purge_adm_ask:all")])
|
||||||
|
|
||||||
|
buttons.append([Button.inline("🔙 بازگشت به منوی سیستم", data="hub_system")])
|
||||||
|
return "\n".join(lines), buttons
|
||||||
|
|
||||||
|
async def purge_review_messages(self, channel_id_or_all: str) -> Tuple[int, int, str]:
|
||||||
|
"""
|
||||||
|
Deletes review messages from the specified admin channel (or all admin channels).
|
||||||
|
Returns: (deleted_tg_count, cleared_db_count, summary_message)
|
||||||
|
"""
|
||||||
|
deleted_tg_count = 0
|
||||||
|
cleared_db_count = 0
|
||||||
|
|
||||||
|
if channel_id_or_all == "all":
|
||||||
|
posts = await self.repo.clear_all_review_messages()
|
||||||
|
cleared_db_count = len(posts)
|
||||||
|
by_channel: Dict[int, List[int]] = {}
|
||||||
|
for p in posts:
|
||||||
|
if p.media_path and os.path.exists(p.media_path):
|
||||||
|
try:
|
||||||
|
os.remove(p.media_path)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
ch_id = p.review_channel_id or self.review_channel_id
|
||||||
|
if ch_id and p.review_message_id:
|
||||||
|
by_channel.setdefault(ch_id, []).append(p.review_message_id)
|
||||||
|
|
||||||
|
for ch_id, msg_ids in by_channel.items():
|
||||||
|
for chunk_idx in range(0, len(msg_ids), 100):
|
||||||
|
chunk = msg_ids[chunk_idx:chunk_idx+100]
|
||||||
|
try:
|
||||||
|
await self.client.delete_messages(ch_id, chunk)
|
||||||
|
deleted_tg_count += len(chunk)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Error deleting chunk from channel {ch_id}: {e}")
|
||||||
|
summary = f"🔥 <b>تمام پیامهای کانالهای نظارت پاکسازی شدند:</b>\n\n• 🗑 پیامهای حذفشده از تلگرام: <b>{deleted_tg_count}</b>\n• 🗂 پستهای بایگانیشده در سیستم: <b>{cleared_db_count}</b>"
|
||||||
|
else:
|
||||||
|
ch_id = int(channel_id_or_all)
|
||||||
|
posts = await self.repo.clear_review_messages_for_channel(ch_id)
|
||||||
|
cleared_db_count = len(posts)
|
||||||
|
msg_ids = []
|
||||||
|
for p in posts:
|
||||||
|
if p.media_path and os.path.exists(p.media_path):
|
||||||
|
try:
|
||||||
|
os.remove(p.media_path)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if p.review_message_id:
|
||||||
|
msg_ids.append(p.review_message_id)
|
||||||
|
|
||||||
|
for chunk_idx in range(0, len(msg_ids), 100):
|
||||||
|
chunk = msg_ids[chunk_idx:chunk_idx+100]
|
||||||
|
try:
|
||||||
|
await self.client.delete_messages(ch_id, chunk)
|
||||||
|
deleted_tg_count += len(chunk)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Error deleting chunk from channel {ch_id}: {e}")
|
||||||
|
|
||||||
|
summary = f"🗑 <b>پیامهای کانال نظارت <code>{ch_id}</code> پاکسازی شدند:</b>\n\n• 🗑 پیامهای حذفشده از تلگرام: <b>{deleted_tg_count}</b>\n• 🗂 پستهای بایگانیشده در سیستم: <b>{cleared_db_count}</b>"
|
||||||
|
|
||||||
|
return deleted_tg_count, cleared_db_count, summary
|
||||||
|
|
||||||
async def _render_monitor_hub(self) -> Tuple[str, List[List[Button]]]:
|
async def _render_monitor_hub(self) -> Tuple[str, List[List[Button]]]:
|
||||||
rep = await get_instant_metrics_report()
|
rep = await get_instant_metrics_report()
|
||||||
buttons = [
|
buttons = [
|
||||||
@@ -1597,6 +1834,22 @@ class AdminBotService:
|
|||||||
text, buttons = await self._render_categories_menu()
|
text, buttons = await self._render_categories_menu()
|
||||||
await event.reply(text, parse_mode="html", buttons=buttons or None)
|
await event.reply(text, parse_mode="html", buttons=buttons or None)
|
||||||
|
|
||||||
|
# --- Admin Review Channels Management ---
|
||||||
|
@self.client.on(events.NewMessage(pattern=r"(?i)^(/admin_channels|/review_channels|📋 کانالهای نظارت|📋 کانالهای نظارت ادمین)$"))
|
||||||
|
async def cmd_admin_channels(event: events.NewMessage.Event):
|
||||||
|
if not self.is_admin(event.sender_id):
|
||||||
|
return
|
||||||
|
text, buttons = await self._render_admin_channels_menu()
|
||||||
|
await event.reply(text, parse_mode="html", buttons=buttons or None)
|
||||||
|
|
||||||
|
# --- Purge Review Channels ---
|
||||||
|
@self.client.on(events.NewMessage(pattern=r"(?i)^(/purge|/clear_reviews|🗑 پاکسازی کانالهای ادمین|🗑 پاکسازی کانالهای نظارت)$"))
|
||||||
|
async def cmd_purge_admin_channels(event: events.NewMessage.Event):
|
||||||
|
if not self.is_admin(event.sender_id):
|
||||||
|
return
|
||||||
|
text, buttons = await self._render_purge_admin_channels_menu()
|
||||||
|
await event.reply(text, parse_mode="html", buttons=buttons or None)
|
||||||
|
|
||||||
# --- Unified Add Hub ---
|
# --- Unified Add Hub ---
|
||||||
@self.client.on(events.NewMessage(pattern=r"(?i)^(/add|➕ افزودن مبدا / مقصد)$"))
|
@self.client.on(events.NewMessage(pattern=r"(?i)^(/add|➕ افزودن مبدا / مقصد)$"))
|
||||||
async def cmd_add_hub(event: events.NewMessage.Event):
|
async def cmd_add_hub(event: events.NewMessage.Event):
|
||||||
@@ -1834,6 +2087,33 @@ class AdminBotService:
|
|||||||
)
|
)
|
||||||
await event.reply(card, parse_mode="html", buttons=buttons)
|
await event.reply(card, parse_mode="html", buttons=buttons)
|
||||||
|
|
||||||
|
# 2.1. Waiting for Admin Review Channel Forward or Text
|
||||||
|
elif action == "wait_admin_channel_fwd":
|
||||||
|
ch_id, title, username, err = await self._resolve_channel(event)
|
||||||
|
if err or not ch_id:
|
||||||
|
await event.reply(err or "خطا در دریافت اطلاعات کانال نظارت. لطفا مجدد ارسال کنید:", buttons=get_cancel_button())
|
||||||
|
return
|
||||||
|
|
||||||
|
src_id = state.get("src_id")
|
||||||
|
site_id = state.get("site_id")
|
||||||
|
self.user_states.pop(event.sender_id, None)
|
||||||
|
adm_id = await self.repo.add_admin_channel(channel_id=ch_id, title=title, username=username)
|
||||||
|
|
||||||
|
if src_id:
|
||||||
|
await self.repo.set_source_admin_channel(src_id, ch_id)
|
||||||
|
card, buttons = await self._render_source_config(src_id)
|
||||||
|
await event.reply(f"✅ کانال نظارت <b>{title}</b> با شناسه <code>{ch_id}</code> افزوده و به کانال مبدا اختصاص داده شد!", parse_mode="html")
|
||||||
|
await event.reply(card, parse_mode="html", buttons=buttons)
|
||||||
|
elif site_id:
|
||||||
|
await self.repo.set_website_admin_channel(site_id, ch_id)
|
||||||
|
card, buttons = await self._render_website_config(site_id)
|
||||||
|
await event.reply(f"✅ کانال نظارت <b>{title}</b> با شناسه <code>{ch_id}</code> افزوده و به وبسایت مبدا اختصاص داده شد!", parse_mode="html")
|
||||||
|
await event.reply(card, parse_mode="html", buttons=buttons)
|
||||||
|
else:
|
||||||
|
card, buttons = await self._render_admin_channel_detail(adm_id)
|
||||||
|
await event.reply(f"✅ کانال نظارت ادمین <b>{title}</b> با شناسه <code>{ch_id}</code> با موفقیت افزوده شد!", parse_mode="html")
|
||||||
|
await event.reply(card, parse_mode="html", buttons=buttons)
|
||||||
|
|
||||||
# 3. Waiting for Target Personality
|
# 3. Waiting for Target Personality
|
||||||
elif action == "wait_personality":
|
elif action == "wait_personality":
|
||||||
target_id = state.get("target_id")
|
target_id = state.get("target_id")
|
||||||
@@ -3128,6 +3408,130 @@ class AdminBotService:
|
|||||||
await event.edit(card, parse_mode="html", buttons=buttons)
|
await event.edit(card, parse_mode="html", buttons=buttons)
|
||||||
await event.answer("✅ دستهبندی کانال با موفقیت به روز شد!")
|
await event.answer("✅ دستهبندی کانال با موفقیت به روز شد!")
|
||||||
|
|
||||||
|
# --- Admin Review Channels Management Callbacks ---
|
||||||
|
elif data == "list_adm_channels":
|
||||||
|
text, buttons = await self._render_admin_channels_menu()
|
||||||
|
await event.edit(text, parse_mode="html", buttons=buttons)
|
||||||
|
await event.answer()
|
||||||
|
|
||||||
|
elif data == "add_adm_ch":
|
||||||
|
self.user_states[event.sender_id] = {"action": "wait_admin_channel_fwd"}
|
||||||
|
guide = (
|
||||||
|
"📋 <b>افزودن کانال نظارت و بازبینی ادمین:</b>\n\n"
|
||||||
|
"سادهترین روش: <b>یک پیام از کانال نظارت مورد نظر به این ربات فوروارد (Forward) کنید!</b>\n\n"
|
||||||
|
"<i>(یا میتوانید آیدی، یوزرنیم یا لینک کانال مثل @admin_review_channel را ارسال نمایید. مطمئن شوید ربات در آن کانال ادمین با دسترسی ارسال و حذف پیام است)</i>"
|
||||||
|
)
|
||||||
|
await event.reply(guide, parse_mode="html", buttons=get_cancel_button())
|
||||||
|
await event.answer()
|
||||||
|
|
||||||
|
elif data.startswith("adm_view:"):
|
||||||
|
adm_id = int(data.split(":")[1])
|
||||||
|
text, buttons = await self._render_admin_channel_detail(adm_id)
|
||||||
|
await event.edit(text, parse_mode="html", buttons=buttons)
|
||||||
|
await event.answer()
|
||||||
|
|
||||||
|
elif data.startswith("adm_set_def:"):
|
||||||
|
adm_id = int(data.split(":")[1])
|
||||||
|
await self.repo.set_default_admin_channel(adm_id)
|
||||||
|
text, buttons = await self._render_admin_channel_detail(adm_id)
|
||||||
|
await event.edit(text, parse_mode="html", buttons=buttons)
|
||||||
|
await event.answer("⭐️ کانال بهعنوان پیشفرض سیستم تنظیم شد!")
|
||||||
|
|
||||||
|
elif data.startswith("adm_del:"):
|
||||||
|
adm_id = int(data.split(":")[1])
|
||||||
|
await self.repo.delete_admin_channel(adm_id)
|
||||||
|
text, buttons = await self._render_admin_channels_menu()
|
||||||
|
await event.edit(f"🗑 <b>کانال نظارت ادمین حذف شد.</b>\n\n{text}", parse_mode="html", buttons=buttons)
|
||||||
|
await event.answer("کانال نظارت حذف شد.")
|
||||||
|
|
||||||
|
# --- Source / Website Admin Channel Assignment Callbacks ---
|
||||||
|
elif data.startswith("src_adm:"):
|
||||||
|
src_id = int(data.split(":")[1])
|
||||||
|
text, buttons = await self._render_source_admin_channel_menu(src_id)
|
||||||
|
await event.edit(text, parse_mode="html", buttons=buttons)
|
||||||
|
await event.answer()
|
||||||
|
|
||||||
|
elif data.startswith("src_set_adm:"):
|
||||||
|
_, src_id_str, adm_ch_str = data.split(":")
|
||||||
|
src_id = int(src_id_str)
|
||||||
|
adm_ch = None if adm_ch_str == "none" else int(adm_ch_str)
|
||||||
|
await self.repo.set_source_admin_channel(src_id, adm_ch)
|
||||||
|
card, buttons = await self._render_source_config(src_id)
|
||||||
|
await event.edit(card, parse_mode="html", buttons=buttons)
|
||||||
|
await event.answer("✅ کانال نظارت برای این مبدا تنظیم شد!")
|
||||||
|
|
||||||
|
elif data.startswith("src_add_adm:"):
|
||||||
|
src_id = int(data.split(":")[1])
|
||||||
|
self.user_states[event.sender_id] = {"action": "wait_admin_channel_fwd", "src_id": src_id}
|
||||||
|
guide = (
|
||||||
|
"📋 <b>افزودن کانال نظارت جدید برای این مبدا:</b>\n\n"
|
||||||
|
"یک پیام از کانال نظارت مورد نظر به ربات فوروارد کنید یا یوزرنیم/آیدی آن را بفرستید:"
|
||||||
|
)
|
||||||
|
await event.reply(guide, parse_mode="html", buttons=get_cancel_button())
|
||||||
|
await event.answer()
|
||||||
|
|
||||||
|
elif data.startswith("web_adm:"):
|
||||||
|
site_id = int(data.split(":")[1])
|
||||||
|
text, buttons = await self._render_website_admin_channel_menu(site_id)
|
||||||
|
await event.edit(text, parse_mode="html", buttons=buttons)
|
||||||
|
await event.answer()
|
||||||
|
|
||||||
|
elif data.startswith("web_set_adm:"):
|
||||||
|
_, site_id_str, adm_ch_str = data.split(":")
|
||||||
|
site_id = int(site_id_str)
|
||||||
|
adm_ch = None if adm_ch_str == "none" else int(adm_ch_str)
|
||||||
|
await self.repo.set_website_admin_channel(site_id, adm_ch)
|
||||||
|
card, buttons = await self._render_website_config(site_id)
|
||||||
|
await event.edit(card, parse_mode="html", buttons=buttons)
|
||||||
|
await event.answer("✅ کانال نظارت برای این وبسایت تنظیم شد!")
|
||||||
|
|
||||||
|
elif data.startswith("web_add_adm:"):
|
||||||
|
site_id = int(data.split(":")[1])
|
||||||
|
self.user_states[event.sender_id] = {"action": "wait_admin_channel_fwd", "site_id": site_id}
|
||||||
|
guide = (
|
||||||
|
"📋 <b>افزودن کانال نظارت جدید برای این وبسایت:</b>\n\n"
|
||||||
|
"یک پیام از کانال نظارت مورد نظر به ربات فوروارد کنید یا یوزرنیم/آیدی آن را بفرستید:"
|
||||||
|
)
|
||||||
|
await event.reply(guide, parse_mode="html", buttons=get_cancel_button())
|
||||||
|
await event.answer()
|
||||||
|
|
||||||
|
# --- Purge Review Messages Callbacks ---
|
||||||
|
elif data == "purge_adm_menu":
|
||||||
|
text, buttons = await self._render_purge_admin_channels_menu()
|
||||||
|
await event.edit(text, parse_mode="html", buttons=buttons)
|
||||||
|
await event.answer()
|
||||||
|
|
||||||
|
elif data.startswith("purge_adm_ask:"):
|
||||||
|
target_ch = data.split(":", 1)[1]
|
||||||
|
if target_ch == "all":
|
||||||
|
prompt_text = (
|
||||||
|
"⚠️ <b>هشدار مهم: پاکسازی تمام کانالهای نظارت ادمین!</b>\n\n"
|
||||||
|
"آیا از حذف کامل تمام کارتهای بازبینی و پیامهای ارسالشده در <b>تمام کانالهای نظارت</b> مطمئن هستید؟\n\n"
|
||||||
|
"<i>این عملیات پیامها را از کانالهای تلگرام حذف کرده و وضعیت پستها را بایگانی میکند.</i>"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
prompt_text = (
|
||||||
|
f"⚠️ <b>هشدار: پاکسازی پیامهای کانال نظارت <code>{target_ch}</code></b>\n\n"
|
||||||
|
f"آیا از حذف تمام کارتهای بازبینی ارسالشده به این کانال مطمئن هستید؟"
|
||||||
|
)
|
||||||
|
|
||||||
|
confirm_buttons = [
|
||||||
|
[Button.inline("🗑 بله، پیامها پاکسازی شوند", data=f"purge_adm_do:{target_ch}")],
|
||||||
|
[Button.inline("❌ انصراف و بازگشت", data="purge_adm_menu")]
|
||||||
|
]
|
||||||
|
await event.edit(prompt_text, parse_mode="html", buttons=confirm_buttons)
|
||||||
|
await event.answer()
|
||||||
|
|
||||||
|
elif data.startswith("purge_adm_do:"):
|
||||||
|
target_ch = data.split(":", 1)[1]
|
||||||
|
await event.edit("⏳ <b>در حال پاکسازی پیامها از تلگرام و پایگاهداده...</b>", parse_mode="html", buttons=None)
|
||||||
|
_, _, summary = await self.purge_review_messages(target_ch)
|
||||||
|
back_buttons = [
|
||||||
|
[Button.inline("🗑 بازگشت به منوی پاکسازی", data="purge_adm_menu")],
|
||||||
|
[Button.inline("🔙 منوی سیستم", data="hub_system")]
|
||||||
|
]
|
||||||
|
await event.edit(summary, parse_mode="html", buttons=back_buttons)
|
||||||
|
await event.answer("✅ پاکسازی با موفقیت انجام شد!")
|
||||||
|
|
||||||
# --- Review Channel Buttons ---
|
# --- Review Channel Buttons ---
|
||||||
elif data.startswith("hist:"):
|
elif data.startswith("hist:"):
|
||||||
|
|||||||
Reference in New Issue
Block a user