pmrony commited on
Commit
fdfab05
·
verified ·
1 Parent(s): 4b06b2f

Upload core.py

Browse files
Files changed (1) hide show
  1. core.py +444 -0
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 ---