Spaces:
Running
Running
Upload core.py
Browse files
core.py
ADDED
|
@@ -0,0 +1,444 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# --- START OF FILE core.py ---
|
| 2 |
+
import asyncio
|
| 3 |
+
import html
|
| 4 |
+
import logging
|
| 5 |
+
import re
|
| 6 |
+
import httpx
|
| 7 |
+
import io
|
| 8 |
+
from datetime import datetime, timedelta, timezone
|
| 9 |
+
from urllib.parse import quote
|
| 10 |
+
|
| 11 |
+
from telegram import Update, MessageOriginChannel, ChatMember, Chat, User, InlineKeyboardButton, InlineKeyboardMarkup, ChatPermissions, ReactionTypeEmoji
|
| 12 |
+
from telegram.ext import ContextTypes
|
| 13 |
+
from telegram.constants import ParseMode, MessageEntityType
|
| 14 |
+
from telegram.error import Forbidden, BadRequest
|
| 15 |
+
|
| 16 |
+
import config
|
| 17 |
+
import db
|
| 18 |
+
from . import utils
|
| 19 |
+
from .batch import BatchDeleter
|
| 20 |
+
|
| 21 |
+
logger = logging.getLogger(__name__)
|
| 22 |
+
|
| 23 |
+
# --- ১. হেল্পার ফাংশনসমূহ ---
|
| 24 |
+
|
| 25 |
+
async def extract_text_with_own_api(image_bytes: bytes, api_url: str) -> str:
|
| 26 |
+
try:
|
| 27 |
+
files = {'file': ('image.jpg', image_bytes, 'image/jpeg')}
|
| 28 |
+
async with httpx.AsyncClient() as client:
|
| 29 |
+
response = await client.post(api_url, files=files, timeout=60.0)
|
| 30 |
+
if response.status_code == 200:
|
| 31 |
+
return response.json().get("extracted_text", "").strip()
|
| 32 |
+
return ""
|
| 33 |
+
except Exception as e:
|
| 34 |
+
logger.error(f"OCR API Error: {e}")
|
| 35 |
+
return ""
|
| 36 |
+
|
| 37 |
+
async def scheduled_delete(context, chat_id, message_id, delay):
|
| 38 |
+
try:
|
| 39 |
+
await asyncio.sleep(delay)
|
| 40 |
+
await context.bot.delete_message(chat_id=chat_id, message_id=message_id)
|
| 41 |
+
except: pass
|
| 42 |
+
|
| 43 |
+
# 🔥 আপডেট করা স্মার্ট লক (ট্রিগার মেসেজ আইডি সেভ করবে)
|
| 44 |
+
async def alert_missing_permissions(context, chat_id, user_id, trigger_msg_id=None):
|
| 45 |
+
redis = utils.get_redis_client_for_chat(context, chat_id)
|
| 46 |
+
if not redis: return
|
| 47 |
+
if await redis.get(f"perm_warn:{chat_id}"): return
|
| 48 |
+
try:
|
| 49 |
+
lang = await db.get_user_lang_from_db(context, user_id)
|
| 50 |
+
msg_text = utils.get_text("deletion_fail_group_warning", lang)
|
| 51 |
+
sent = await context.bot.send_message(chat_id, msg_text, parse_mode='HTML')
|
| 52 |
+
|
| 53 |
+
await redis.set(f"perm_warn:{chat_id}", "1", ex=3600)
|
| 54 |
+
await redis.set(f"perm_warn_msg_id:{chat_id}", str(sent.message_id), ex=3600)
|
| 55 |
+
# যে লিংকটি ডিলিট করতে পারেনি সেটিও সেভ রাখা হলো
|
| 56 |
+
if trigger_msg_id:
|
| 57 |
+
await redis.set(f"perm_trigger_msg_id:{chat_id}", str(trigger_msg_id), ex=3600)
|
| 58 |
+
except: pass
|
| 59 |
+
|
| 60 |
+
# --- ২. ডিলিট করার মূল লজিক ---
|
| 61 |
+
|
| 62 |
+
async def process_deletion_and_tasks(context, user, chat, msg_id, settings, reason_keys, content):
|
| 63 |
+
batch_deleter = context.bot_data.get('batch_deleter')
|
| 64 |
+
if batch_deleter:
|
| 65 |
+
await batch_deleter.delete(chat.id, msg_id)
|
| 66 |
+
else:
|
| 67 |
+
try:
|
| 68 |
+
await context.bot.delete_message(chat.id, msg_id)
|
| 69 |
+
except BadRequest as e:
|
| 70 |
+
if "not found" not in str(e).lower():
|
| 71 |
+
asyncio.create_task(alert_missing_permissions(context, chat.id, user.id, msg_id))
|
| 72 |
+
except Forbidden:
|
| 73 |
+
asyncio.create_task(alert_missing_permissions(context, chat.id, user.id, msg_id))
|
| 74 |
+
|
| 75 |
+
asyncio.create_task(_background_processing(context, user, chat, settings, reason_keys, content))
|
| 76 |
+
|
| 77 |
+
async def _background_processing(context, user, chat, settings, reason_keys, content):
|
| 78 |
+
redis_client = utils.get_redis_client_for_chat(context, chat.id)
|
| 79 |
+
is_join_leave = 'censor_reason_join_leave' in reason_keys
|
| 80 |
+
is_bot_link = False
|
| 81 |
+
|
| 82 |
+
if 'censor_reason_link' in reason_keys or 'censor_reason_bio_link' in reason_keys:
|
| 83 |
+
if re.search(r'(@[\w_]+bot\b|(?:t\.me|telegram\.me)\/[\w_]+bot\b)', content, re.IGNORECASE):
|
| 84 |
+
is_bot_link = True
|
| 85 |
+
|
| 86 |
+
if not is_join_leave and not is_bot_link:
|
| 87 |
+
if re.search(r'(?:t\.me|telegram\.me)', content, re.IGNORECASE) or 'censor_reason_word' in reason_keys:
|
| 88 |
+
asyncio.create_task(utils.send_deletion_report(context, user, chat, reason_keys[0], content))
|
| 89 |
+
|
| 90 |
+
await utils.update_user_activity_score(context, chat.id, user.id, 'deletion', reason_keys, content)
|
| 91 |
+
|
| 92 |
+
if settings.get('mute_on_link_24_h', False) and ('censor_reason_link' in reason_keys or 'censor_reason_bio_link' in reason_keys):
|
| 93 |
+
try:
|
| 94 |
+
mute_until = datetime.now(timezone.utc) + timedelta(hours=24)
|
| 95 |
+
await context.bot.restrict_chat_member(chat.id, user.id, ChatPermissions(can_send_messages=False), until_date=mute_until)
|
| 96 |
+
except: pass
|
| 97 |
+
|
| 98 |
+
if redis_client:
|
| 99 |
+
cooldown_key = f"warn_cooldown:{chat.id}:{user.id}"
|
| 100 |
+
if not await redis_client.get(cooldown_key):
|
| 101 |
+
try:
|
| 102 |
+
lang = await db.get_user_lang_from_db(context, user.id)
|
| 103 |
+
reason_msg = utils.get_text(reason_keys[0], lang)
|
| 104 |
+
warn_text = utils.get_text("censor_warning", lang).format(user_mention=user.mention_html(), reason=reason_msg, bot_username=context.bot.username)
|
| 105 |
+
sent_warn = await context.bot.send_message(chat.id, warn_text, parse_mode=ParseMode.HTML)
|
| 106 |
+
await redis_client.set(cooldown_key, "1", ex=600)
|
| 107 |
+
asyncio.create_task(utils.delete_message_after_delay(context, chat.id, sent_warn.message_id, config.CENSOR_WARNING_DELETE_SECONDS))
|
| 108 |
+
except: pass
|
| 109 |
+
|
| 110 |
+
async def process_image_with_ocr_in_background(context, chat_id, user_id, message_id, image_bytes):
|
| 111 |
+
try:
|
| 112 |
+
ocr_semaphore = context.bot_data.get('ocr_semaphore')
|
| 113 |
+
async with ocr_semaphore:
|
| 114 |
+
ocr_text = await extract_text_with_own_api(image_bytes, config.OCR_API_URL)
|
| 115 |
+
if ocr_text:
|
| 116 |
+
normalized_text = re.sub(r'\s+', '', ocr_text).lower()
|
| 117 |
+
if config.LINK_PATTERN_COMPILED.search(ocr_text) or any(indicator in normalized_text for indicator in config.LINK_INDICATOR_KEYWORDS):
|
| 118 |
+
settings = await utils.get_settings_from_cache_and_update_regex(context, chat_id)
|
| 119 |
+
try:
|
| 120 |
+
chat_obj = await context.bot.get_chat(chat_id)
|
| 121 |
+
member = await context.bot.get_chat_member(chat_id, user_id)
|
| 122 |
+
asyncio.create_task(process_deletion_and_tasks(context, member.user, chat_obj, message_id, settings, ['censor_reason_link'], ocr_text))
|
| 123 |
+
except: pass
|
| 124 |
+
except: pass
|
| 125 |
+
|
| 126 |
+
# --- ৩. প্রধান মেসেজ হ্যান্ডেলার ---
|
| 127 |
+
|
| 128 |
+
async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
| 129 |
+
msg, chat, user = update.effective_message, update.effective_chat, update.effective_user
|
| 130 |
+
if not all([msg, chat, user]): return
|
| 131 |
+
|
| 132 |
+
redis = utils.get_redis_client_for_chat(context, chat.id)
|
| 133 |
+
if redis and await redis.get(f"perm_warn:{chat.id}"): return
|
| 134 |
+
|
| 135 |
+
is_bot_admin = await utils.is_group_admin(context, chat.id, context.bot.id)
|
| 136 |
+
if not is_bot_admin and chat.type != 'private':
|
| 137 |
+
asyncio.create_task(alert_missing_permissions(context, chat.id, user.id, msg.message_id))
|
| 138 |
+
return
|
| 139 |
+
|
| 140 |
+
if chat.type == 'private':
|
| 141 |
+
if not user.is_bot and not msg.new_chat_members:
|
| 142 |
+
asyncio.create_task(db.buffer_user_stats(context, user.id, user.first_name, user.username))
|
| 143 |
+
return
|
| 144 |
+
|
| 145 |
+
if msg.new_chat_members:
|
| 146 |
+
is_adder_admin = await utils.is_group_admin(context, chat.id, user.id)
|
| 147 |
+
for member in msg.new_chat_members:
|
| 148 |
+
if member.is_bot and member.id != context.bot.id and not is_adder_admin:
|
| 149 |
+
try:
|
| 150 |
+
await context.bot.ban_chat_member(chat.id, member.id)
|
| 151 |
+
await msg.delete()
|
| 152 |
+
return
|
| 153 |
+
except: pass
|
| 154 |
+
|
| 155 |
+
is_edit = update.edited_message is not None
|
| 156 |
+
asyncio.create_task(db.track_chat_member(context.bot_data.get('db_pool'), chat.id, user.id))
|
| 157 |
+
if not is_edit:
|
| 158 |
+
asyncio.create_task(db.buffer_user_stats(context, user.id, user.first_name, user.username))
|
| 159 |
+
|
| 160 |
+
settings = await utils.get_settings_from_cache_and_update_regex(context, chat.id)
|
| 161 |
+
owner_id = await utils.get_group_owner_id(context, chat.id)
|
| 162 |
+
is_exempt = (user.id == owner_id) or (user.id == context.bot_data.get('BOT_OWNER_ID')) or (settings.get('allow_admins_manage') and await utils.is_group_admin(context, chat.id, user.id))
|
| 163 |
+
|
| 164 |
+
if is_exempt and not settings.get('enable_traffic_control', False): return
|
| 165 |
+
if settings.get('enable_traffic_control', False): asyncio.create_task(utils.check_traffic_control(context, chat.id))
|
| 166 |
+
if is_exempt: return
|
| 167 |
+
|
| 168 |
+
# --- লিংক এবং মেনশন স্মার্ট চেক ---
|
| 169 |
+
text_content = (msg.text or msg.caption or "").strip()
|
| 170 |
+
# --- লিংক এবং মেনশন স্মার্ট চেক ---
|
| 171 |
+
# আগে শুধু text_content চেক হতো, এখন পুরো 'msg' অবজেক্ট পাঠাবো
|
| 172 |
+
is_link = utils.has_link_in_text(msg)
|
| 173 |
+
|
| 174 |
+
# মেনশন বা ইউজারনেম চেক
|
| 175 |
+
if not is_link:
|
| 176 |
+
text_content = (msg.text or msg.caption or "").strip()
|
| 177 |
+
entities = msg.entities or msg.caption_entities or []
|
| 178 |
+
|
| 179 |
+
# এনটিটির ভেতর মেনশন আছে কি না দেখা
|
| 180 |
+
has_mention_entity = any(ent.type in [MessageEntityType.MENTION, MessageEntityType.TEXT_MENTION] for ent in entities)
|
| 181 |
+
|
| 182 |
+
if has_mention_entity:
|
| 183 |
+
if not settings.get('allow_usernames', False):
|
| 184 |
+
is_link = True # ইউজারনেম এলাউ না থাকলে ডিলিট হবে
|
| 185 |
+
else:
|
| 186 |
+
# যদি এলাউ থাকে কিন্তু সেটি চ্যানেল কি না চেক করা
|
| 187 |
+
mentions = config.MENTION_PATTERN_COMPILED.findall(text_content)
|
| 188 |
+
for mention in mentions:
|
| 189 |
+
if await utils.is_channel_or_group_username(context, mention):
|
| 190 |
+
is_link = True
|
| 191 |
+
break
|
| 192 |
+
|
| 193 |
+
# --- ১৮৫-১৯২ এর পর যেখানে violations চেক শুরু হবে ---
|
| 194 |
+
violations = []
|
| 195 |
+
|
| 196 |
+
# ১. লিংক ভায়োলেশন অ্যাড করা (এটি যোগ করা জরুরি)
|
| 197 |
+
if is_link:
|
| 198 |
+
violations.append(('link', 'censor_reason_link'))
|
| 199 |
+
|
| 200 |
+
# ২. নিষিদ্ধ শব্দ চেক
|
| 201 |
+
if (fw_regex := settings.get('fw_regex')) and fw_regex.search(text_content):
|
| 202 |
+
violations.append(('word', 'censor_reason_word'))
|
| 203 |
+
|
| 204 |
+
# ৩. ফরোয়ার্ড মেসেজ চেক
|
| 205 |
+
if settings.get('block_channel_forwards') and isinstance(msg.forward_origin, MessageOriginChannel):
|
| 206 |
+
violations.append(('forward', 'censor_reason_forward'))
|
| 207 |
+
|
| 208 |
+
# ৪. কাউন্টার এবং মিউট লজিক (সবার জন্য)
|
| 209 |
+
if violations:
|
| 210 |
+
if redis:
|
| 211 |
+
spam_key = f"spam_violation_counter:{chat.id}:{user.id}"
|
| 212 |
+
current_count = await redis.incr(spam_key)
|
| 213 |
+
if current_count == 1:
|
| 214 |
+
await redis.expire(spam_key, 600) # ১০ মিনিট
|
| 215 |
+
|
| 216 |
+
if current_count >= 11:
|
| 217 |
+
try:
|
| 218 |
+
# পারমানেন্ট মিউট
|
| 219 |
+
await context.bot.restrict_chat_member(chat.id, user.id, ChatPermissions(can_send_messages=False))
|
| 220 |
+
except: pass
|
| 221 |
+
|
| 222 |
+
# ডিলিট এবং রিপোর্ট পাঠানোর টাস্ক
|
| 223 |
+
asyncio.create_task(process_deletion_and_tasks(context, user, chat, msg.message_id, settings, [v[1] for v in violations], text_content))
|
| 224 |
+
return
|
| 225 |
+
|
| 226 |
+
# --- স্প্যাম স্ক্যানার (ওনার মেনশন ও বাটন জাম্প লিঙ্ক) ---
|
| 227 |
+
if settings.get('enable_spamscan') and config.SPAM_PHRASES_COMPILED.search(text_content):
|
| 228 |
+
try:
|
| 229 |
+
# ১. স্মার্ট জাম্প লিঙ্ক তৈরি (সরাসরি আসল স্প্যাম মেসেজে যাওয়ার জন্য)
|
| 230 |
+
if chat.username:
|
| 231 |
+
jump_url = f"https://t.me/{chat.username}/{msg.message_id}"
|
| 232 |
+
else:
|
| 233 |
+
chat_id_clean = str(chat.id).replace("-100", "")
|
| 234 |
+
jump_url = f"https://t.me/c/{chat_id_clean}/{msg.message_id}"
|
| 235 |
+
|
| 236 |
+
# ২. গ্রুপ ওনারকে খুঁজে বের করা (যাতে নোটিফিকেশন যায়)
|
| 237 |
+
owner_id = await utils.get_group_owner_id(context, chat.id)
|
| 238 |
+
mention_text = ""
|
| 239 |
+
if owner_id:
|
| 240 |
+
# ওনারের আইডি দিয়ে লিঙ্ক তৈরি, এতে 100% নোটিফিকেশন বাজবে
|
| 241 |
+
mention_text = f"📢 <b>Attention:</b> <a href='tg://user?id={owner_id}'>Admin</a>\n"
|
| 242 |
+
|
| 243 |
+
# ৩. ক্লিন অ্যালার্ট টেক্সট
|
| 244 |
+
warn_text = f"🚨 <b>Spam Alert!</b>\n{mention_text}⚠️ একটি স্প্যাম মেসেজ পাওয়া গেছে।"
|
| 245 |
+
|
| 246 |
+
# ৪. বাটন সেটআপ (এখানে ব্র্যাকেট ফিক্স করা হয়েছে)
|
| 247 |
+
btns = [[InlineKeyboardButton("👀 View Message", url=jump_url)],[InlineKeyboardButton("🔇 Mute & Delete", callback_data=f"spam_act_mute_{user.id}_{msg.message_id}"),
|
| 248 |
+
InlineKeyboardButton("❌ Cancel", callback_data="spam_act_cancel")]
|
| 249 |
+
]
|
| 250 |
+
|
| 251 |
+
# ৫. মেসেজ পাঠানো (কোনো রিপ্লাই বা নীল বক্স আসবে না)
|
| 252 |
+
await context.bot.send_message(
|
| 253 |
+
chat_id=chat.id,
|
| 254 |
+
text=warn_text,
|
| 255 |
+
parse_mode=ParseMode.HTML,
|
| 256 |
+
reply_markup=InlineKeyboardMarkup(btns),
|
| 257 |
+
disable_web_page_preview=True
|
| 258 |
+
)
|
| 259 |
+
|
| 260 |
+
except Exception as e:
|
| 261 |
+
logger.error(f"Spam Scan Error: {e}")
|
| 262 |
+
return
|
| 263 |
+
|
| 264 |
+
if settings.get('enable_sentiment', False) and text_content:
|
| 265 |
+
senti = await asyncio.to_thread(utils.analyze_sentiment, text_content)
|
| 266 |
+
if senti:
|
| 267 |
+
try: await msg.set_reaction(reaction=[ReactionTypeEmoji("❤️" if senti == 'positive' else "👎")])
|
| 268 |
+
except: pass
|
| 269 |
+
|
| 270 |
+
if settings.get('block_bio_links', True):
|
| 271 |
+
bio_status_key = f"bio_status:{user.id}"
|
| 272 |
+
cached_bio = await redis.get(bio_status_key) if redis else None
|
| 273 |
+
if cached_bio == "bad":
|
| 274 |
+
asyncio.create_task(process_deletion_and_tasks(context, user, chat, msg.message_id, settings, ['censor_reason_bio_link'], "Bio contains links (cached)"))
|
| 275 |
+
return
|
| 276 |
+
if not cached_bio:
|
| 277 |
+
async with context.bot_data.get('api_semaphore'):
|
| 278 |
+
try:
|
| 279 |
+
profile = await context.bot.get_chat(user.id)
|
| 280 |
+
if profile.bio and utils.has_link_in_text(profile.bio):
|
| 281 |
+
if redis: await redis.set(bio_status_key, "bad", ex=600)
|
| 282 |
+
asyncio.create_task(process_deletion_and_tasks(context, user, chat, msg.message_id, settings, ['censor_reason_bio_link'], f"Bio: {profile.bio}"))
|
| 283 |
+
return
|
| 284 |
+
if redis: await redis.set(bio_status_key, "safe", ex=600)
|
| 285 |
+
except: pass
|
| 286 |
+
|
| 287 |
+
if settings.get('delete_join_messages') and (msg.new_chat_members or msg.left_chat_member):
|
| 288 |
+
asyncio.create_task(process_deletion_and_tasks(context, user, chat, msg.message_id, settings, ['censor_reason_join_leave'], "Join/Leave message"))
|
| 289 |
+
return
|
| 290 |
+
|
| 291 |
+
if text_content:
|
| 292 |
+
filters_cache = await db.get_chat_filters(context, chat.id)
|
| 293 |
+
for keyword, fdata in filters_cache.items():
|
| 294 |
+
if keyword in text_content.lower():
|
| 295 |
+
args = {'chat_id': chat.id, 'reply_to_message_id': msg.message_id}
|
| 296 |
+
if fdata['type'] == 'photo': await context.bot.send_photo(photo=fdata['file_id'], caption=fdata['text'], **args)
|
| 297 |
+
else: await context.bot.send_message(text=fdata['text'], parse_mode=ParseMode.HTML, **args)
|
| 298 |
+
return
|
| 299 |
+
if text_content.startswith('#'):
|
| 300 |
+
if note_data := await db.get_note_from_db(context, chat.id, text_content.split(' ')[0][1:].lower()):
|
| 301 |
+
await context.bot.send_message(chat.id, note_data['text'], parse_mode=ParseMode.HTML, reply_to_message_id=msg.message_id)
|
| 302 |
+
|
| 303 |
+
auto_del = settings.get('auto_delete_seconds', 0)
|
| 304 |
+
if auto_del > 0:
|
| 305 |
+
is_media = (msg.photo or msg.video or msg.document or msg.voice or msg.audio or msg.sticker or msg.animation or msg.video_note)
|
| 306 |
+
if is_media: asyncio.create_task(scheduled_delete(context, chat.id, msg.message_id, auto_del))
|
| 307 |
+
|
| 308 |
+
if settings.get('enable_ocr_scan') and msg.photo and config.OCR_API_URL:
|
| 309 |
+
try:
|
| 310 |
+
photo_file = await msg.photo[-1].get_file()
|
| 311 |
+
photo_bytes = await photo_file.download_as_bytearray()
|
| 312 |
+
asyncio.create_task(process_image_with_ocr_in_background(context, chat_id, user.id, msg.message_id, bytes(photo_bytes)))
|
| 313 |
+
except: pass
|
| 314 |
+
|
| 315 |
+
# --- ৮. মেম্বার ও বটের স্ট্যাটাস হ্যান্ডেলার ---
|
| 316 |
+
|
| 317 |
+
async def handle_member_status_change(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
| 318 |
+
if not update.chat_member: return
|
| 319 |
+
chat = update.chat_member.chat
|
| 320 |
+
user = update.chat_member.new_chat_member.user
|
| 321 |
+
new_status = update.chat_member.new_chat_member.status
|
| 322 |
+
old_status = update.chat_member.old_chat_member.status
|
| 323 |
+
redis = utils.get_redis_client_for_chat(context, chat.id)
|
| 324 |
+
|
| 325 |
+
if redis:
|
| 326 |
+
if new_status in [ChatMember.ADMINISTRATOR, ChatMember.OWNER]:
|
| 327 |
+
await redis.set(f"is_admin:{chat.id}:{user.id}", "1", ex=86400)
|
| 328 |
+
else:
|
| 329 |
+
await redis.set(f"is_admin:{chat.id}:{user.id}", "0", ex=86400)
|
| 330 |
+
await redis.delete(f"bio_status:{user.id}")
|
| 331 |
+
if new_status == ChatMember.OWNER:
|
| 332 |
+
await redis.set(f"group_owner:{chat.id}", str(user.id), ex=86400)
|
| 333 |
+
|
| 334 |
+
if new_status == ChatMember.MEMBER and old_status not in [ChatMember.MEMBER, ChatMember.ADMINISTRATOR, ChatMember.OWNER] and not user.is_bot:
|
| 335 |
+
await db._add_user_to_db_core(context.bot_data.get('db_pool'), user.id, user.first_name, user.username)
|
| 336 |
+
|
| 337 |
+
try:
|
| 338 |
+
settings = await utils.get_settings_from_cache_and_update_regex(context, chat.id)
|
| 339 |
+
welcome_data = settings.get('welcome_data') or {}
|
| 340 |
+
|
| 341 |
+
if redis:
|
| 342 |
+
last_msg_id = await redis.get(f"last_welcome_msg:{chat.id}")
|
| 343 |
+
if last_msg_id:
|
| 344 |
+
try: await context.bot.delete_message(chat.id, int(last_msg_id))
|
| 345 |
+
except: pass
|
| 346 |
+
|
| 347 |
+
raw_text = welcome_data.get('text') or (
|
| 348 |
+
"👋 <b>Welcome {name} to our Group!</b>\n\n"
|
| 349 |
+
"🛡️ I am your <b>Group Protector</b>. I will keep this chat safe from spam.\n"
|
| 350 |
+
"📝 Please follow the rules to avoid being muted or banned."
|
| 351 |
+
)
|
| 352 |
+
welcome_text = raw_text.replace("{name}", user.mention_html())
|
| 353 |
+
|
| 354 |
+
# --- স্মার্ট বাটন জেনারেট লজিক (এনকোডিং ফিক্স সহ) ---
|
| 355 |
+
buttons_list = welcome_data.get('buttons', [])
|
| 356 |
+
keyboard = []
|
| 357 |
+
for i, btn in enumerate(buttons_list):
|
| 358 |
+
btn_name = btn['name']
|
| 359 |
+
btn_content = btn['content'].strip()
|
| 360 |
+
|
| 361 |
+
if btn_content.startswith('share:'):
|
| 362 |
+
# এনকোডিং ফিক্সের জন্য quote ইমপোর্ট করা হলো
|
| 363 |
+
from urllib.parse import quote
|
| 364 |
+
link_to_share = btn_content.replace('share:', '').strip()
|
| 365 |
+
share_url = f"https://t.me/share/url?url={quote(link_to_share)}"
|
| 366 |
+
keyboard.append([InlineKeyboardButton(btn_name, url=share_url)])
|
| 367 |
+
|
| 368 |
+
elif btn_content.startswith('http'):
|
| 369 |
+
keyboard.append([InlineKeyboardButton(btn_name, url=btn_content)])
|
| 370 |
+
|
| 371 |
+
else:
|
| 372 |
+
keyboard.append([InlineKeyboardButton(btn_name, callback_data=f"w_pop_{i}")])
|
| 373 |
+
|
| 374 |
+
reply_markup = InlineKeyboardMarkup(keyboard) if keyboard else None
|
| 375 |
+
sent = None
|
| 376 |
+
|
| 377 |
+
if settings.get('welcome_mode', 'text') == 'card':
|
| 378 |
+
photos = await user.get_profile_photos(limit=1)
|
| 379 |
+
pfp = None
|
| 380 |
+
if photos.total_count > 0:
|
| 381 |
+
try:
|
| 382 |
+
pfp_file = await photos.photos[0][-1].get_file()
|
| 383 |
+
pfp = bytes(await pfp_file.download_as_bytearray())
|
| 384 |
+
except: pfp = None
|
| 385 |
+
|
| 386 |
+
card = await utils.generate_welcome_card(user.first_name, pfp)
|
| 387 |
+
if card:
|
| 388 |
+
sent = await context.bot.send_photo(chat.id, photo=card, caption=welcome_text, parse_mode=ParseMode.HTML, reply_markup=reply_markup)
|
| 389 |
+
|
| 390 |
+
if not sent:
|
| 391 |
+
sent = await context.bot.send_message(chat.id, text=welcome_text, parse_mode=ParseMode.HTML, disable_web_page_preview=True, reply_markup=reply_markup)
|
| 392 |
+
|
| 393 |
+
if sent and redis:
|
| 394 |
+
await redis.set(f"last_welcome_msg:{chat.id}", sent.message_id)
|
| 395 |
+
ds = welcome_data.get('delete_seconds', 0)
|
| 396 |
+
if ds > 0:
|
| 397 |
+
asyncio.create_task(utils.delete_message_after_delay(context, chat.id, sent.message_id, ds))
|
| 398 |
+
|
| 399 |
+
except Exception as e:
|
| 400 |
+
logger.error(f"Welcome Error: {e}")
|
| 401 |
+
|
| 402 |
+
# 🔥 অটো আনলক ও অটো-ডিলিট ট্রিগার মেসেজ
|
| 403 |
+
async def handle_bot_status_change(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
| 404 |
+
result = update.my_chat_member
|
| 405 |
+
if not result: return
|
| 406 |
+
chat = result.chat
|
| 407 |
+
new_status = result.new_chat_member.status
|
| 408 |
+
pool = context.bot_data.get('db_pool')
|
| 409 |
+
|
| 410 |
+
if new_status in [ChatMember.MEMBER, ChatMember.ADMINISTRATOR]:
|
| 411 |
+
await db._add_chat_to_db_core(pool, chat.id)
|
| 412 |
+
if new_status == ChatMember.ADMINISTRATOR:
|
| 413 |
+
redis = utils.get_redis_client_for_chat(context, chat.id)
|
| 414 |
+
if redis:
|
| 415 |
+
# ১. বটের নিজের Admin Status Cache আপডেট করা (যাতে সাথে সাথে কাজ শুরু করে)
|
| 416 |
+
await redis.set(f"is_admin:{chat.id}:{context.bot.id}", "1", ex=86400)
|
| 417 |
+
|
| 418 |
+
# ২. পারমিশন ওয়ার্নিং লক রিলিজ করা
|
| 419 |
+
await redis.delete(f"perm_warn:{chat.id}")
|
| 420 |
+
|
| 421 |
+
# ৩. আগের ওয়ার্নিং মেসেজ ডিলিট করা
|
| 422 |
+
if (last_warn := await redis.get(f"perm_warn_msg_id:{chat.id}")):
|
| 423 |
+
try:
|
| 424 |
+
await context.bot.delete_message(chat.id, int(last_warn))
|
| 425 |
+
await redis.delete(f"perm_warn_msg_id:{chat.id}")
|
| 426 |
+
except: pass
|
| 427 |
+
|
| 428 |
+
# ৪. 🔥 যে লিংকটির কারণে পারমিশন এরর এসেছিল সেটি ডিলিট করা
|
| 429 |
+
if (trigger_link_id := await redis.get(f"perm_trigger_msg_id:{chat.id}")):
|
| 430 |
+
try:
|
| 431 |
+
await context.bot.delete_message(chat.id, int(trigger_link_id))
|
| 432 |
+
await redis.delete(f"perm_trigger_msg_id:{chat.id}")
|
| 433 |
+
except: pass
|
| 434 |
+
|
| 435 |
+
elif new_status in [ChatMember.LEFT, ChatMember.KICKED, ChatMember.BANNED]:
|
| 436 |
+
if pool:
|
| 437 |
+
try:
|
| 438 |
+
async with pool.acquire() as conn: await conn.execute("DELETE FROM chats WHERE chat_id = $1", chat.id)
|
| 439 |
+
except: pass
|
| 440 |
+
|
| 441 |
+
async def handle_edited_message(update, context):
|
| 442 |
+
await handle_message(update, context)
|
| 443 |
+
|
| 444 |
+
# --- END OF FILE ---
|