File size: 15,799 Bytes
780e11f
 
 
d0e5242
7663da4
d0e5242
 
780e11f
 
bb12bea
780e11f
7533da6
bb12bea
 
 
 
 
 
3356b5c
780e11f
3356b5c
780e11f
 
 
7533da6
780e11f
7663da4
780e11f
bb12bea
780e11f
 
7663da4
780e11f
4583634
d0e5242
 
 
 
bb12bea
d0e5242
 
 
 
 
bb12bea
780e11f
 
 
4583634
7663da4
780e11f
 
7533da6
780e11f
 
 
 
bb12bea
780e11f
 
 
 
 
 
bb12bea
 
 
 
 
 
 
780e11f
bb12bea
7533da6
780e11f
50b342b
 
 
 
 
 
 
7663da4
 
 
50b342b
 
 
7663da4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50b342b
7663da4
 
 
 
 
780e11f
 
 
bb12bea
 
 
 
 
 
 
 
 
7533da6
 
 
 
 
 
 
 
 
 
 
 
 
bb12bea
 
 
 
4583634
bb12bea
7663da4
bb12bea
50b342b
7533da6
 
 
 
 
 
 
 
780e11f
 
bb12bea
 
780e11f
7663da4
780e11f
4583634
780e11f
bb12bea
 
 
780e11f
bb12bea
 
780e11f
bb12bea
7533da6
bb12bea
 
7533da6
bb12bea
 
 
780e11f
bb12bea
780e11f
4583634
780e11f
7663da4
 
 
 
 
780e11f
 
50b342b
 
bb12bea
 
 
 
 
7663da4
780e11f
 
bb12bea
780e11f
 
4583634
 
 
bb12bea
780e11f
d0e5242
bb12bea
 
 
 
 
 
 
 
4583634
bb12bea
 
4583634
 
bb12bea
4583634
bb12bea
4583634
 
bb12bea
4583634
 
 
 
 
 
 
 
 
bb12bea
4583634
bb12bea
 
4583634
 
 
 
 
 
 
 
bb12bea
780e11f
bb12bea
 
 
 
 
d0e5242
7533da6
780e11f
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
import logging
import os
import asyncio
import threading
import re
from fastapi import FastAPI
import uvicorn
import redis.asyncio as redis
from datetime import datetime, timedelta, timezone
from concurrent.futures import ThreadPoolExecutor

# --- OPTIMIZATION: uvloop setup (For faster performance) ---
try:
    import uvloop
    asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
except ImportError:
    pass

from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup, ChatMember, ChatPermissions
from telegram.ext import (
    Application, CommandHandler, MessageHandler, ChatJoinRequestHandler,
    filters, ChatMemberHandler, ContextTypes, CallbackQueryHandler
)

# আপনার প্রজেক্টের ফাইলগুলো থেকে ইম্পোর্ট
from credentials import BOT_TOKEN, BOT_OWNER_ID, REPORT_CHANNEL_ID, REDIS_URL_1, REDIS_URL_2
import config
from config import logger, BOT_VERSION, TELEGRAM_API_CONCURRENCY_LIMIT
from db import create_db_pool_with_retry, setup_database
import db 
from bot_handlers.batch import BatchDeleter 
from bot_handlers import utils

# --- ১. Hugging Face Health Check Server (Port 7860) ---
app_web = FastAPI()

@app_web.get("/")
def read_root():
    return {"status": "Bot is alive and running!", "version": BOT_VERSION}

def run_web_server():
    # Hugging Face ডিফল্ট ৭৮৬০ পোর্ট ব্যবহার করে
    uvicorn.run(app_web, host="0.0.0.0", port=7860)

# --- ২. গ্লোবাল এরর হ্যান্ডেলার ---
async def global_error_handler(update: object, context: ContextTypes.DEFAULT_TYPE):
    logger.error("Exception while handling an update:", exc_info=context.error)

# --- ৩. ব্যাকগ্রাউন্ড টাস্কসমূহ ---

async def user_stats_flush_loop(app):
    while True:
        await asyncio.sleep(300) # প্রতি ৫ মিনিট পর DB-তে ডাটা ফ্লাশ হবে
        await db.flush_user_stats_to_db(app)

async def check_expired_trials_loop(app):
    while True:
        await asyncio.sleep(60) 
        pool = app.bot_data.get('db_pool')
        if not pool: continue
        try:
            async with pool.acquire() as conn:
                now_utc = datetime.now(timezone.utc)
                expired_vips = await conn.fetch("SELECT chat_id, user_id FROM vip_trials WHERE expiry_date < $1 AND is_safe = FALSE", now_utc)
                if expired_vips:
                    for row in expired_vips:
                        try:
                            await app.bot.ban_chat_member(row['chat_id'], row['user_id'])
                            await app.bot.unban_chat_member(row['chat_id'], row['user_id'])
                            await asyncio.sleep(0.5)
                        except: pass
                await conn.execute("DELETE FROM vip_trials WHERE expiry_date < $1", now_utc)
        except Exception as e:
            logger.error(f"❌ VIP Monitor Error: {e}")

async def recovery_traffic_locks(app):
    """বট স্টার্ট হওয়ার সময় চেক করবে কোনো গ্রুপ লক হয়ে আছে কি না"""
    pool = app.bot_data.get('db_pool')
    if not pool: return
    group_ids = await db.get_all_group_ids(pool)
    for chat_id in group_ids:
        from bot_handlers.redis_manager import get_redis_client_for_chat
        redis_client = get_redis_client_for_chat(app, chat_id)
        if redis_client:
            backup = await redis_client.get(f"backup_perms:{chat_id}")
            if backup:
                from bot_handlers.utils import restore_group_permissions
                await restore_group_permissions(app, chat_id, backup)

# 🔥 নতুন: ব্যাকগ্রাউন্ড বায়ো চেকার টাস্ক (একই প্রসেসের ভেতরে চলবে)
async def background_bio_task(app):
    """এটি মেইন বটের ভেতরেই কিউ থেকে আইডি নিয়ে এক এক করে প্রোফাইল চেক করবে"""
    while True:
        try:
            # লাইন (Queue) থেকে ডাটা নিবে
            data = await app.bot_data['bio_queue'].get()
            chat_id, user_id, msg_id, settings, user_obj = data
            
            # রেট লিমিট এড়াতে ০.১ সেকেন্ড বিরতি
            await asyncio.sleep(0.1)
            
            try:
                full_user = await app.bot.get_chat(user_id)
                bio_text = getattr(full_user, 'bio', "") or ""
                first_name = full_user.first_name or ""
                last_name = full_user.last_name or ""
                personal_chat = getattr(full_user, 'personal_chat', None)
                
                full_profile_text = f"{first_name} {last_name} {bio_text}".strip()
                is_bad = False
                reason_text = "Bad Profile (Auto-detected)"
                
                if personal_chat:
                    is_bad = True
                    reason_text = "Personal Channel Linked"
                elif full_profile_text:
                    if utils.has_link_in_text(full_profile_text) or config.SPAM_PHRASES_COMPILED.search(full_profile_text):
                        is_bad = True
                        reason_text = "Link or Spam words in Bio/Name"

                redis_client = app.bot_data.get('redis_clients')[0] if app.bot_data.get('redis_clients') else None
                
                if is_bad:
                    if redis_client: await redis_client.set(f"bio_status:{user_id}", "bad", ex=1800)
                    
                    # মেসেজ ডিলিট
                    try: await app.bot.delete_message(chat_id, msg_id)
                    except: pass
                    
                    # ডিলেশন রিপোর্ট পাঠানো (Optional)
                    try:
                        from bot_handlers.core import _background_processing
                        class DummyContext:
                            def __init__(self, bot, bot_data):
                                self.bot = bot
                                self.bot_data = bot_data
                        dummy_ctx = DummyContext(app.bot, app.bot_data)
                        chat_obj = await app.bot.get_chat(chat_id)
                        asyncio.create_task(_background_processing(dummy_ctx, user_obj, chat_obj, settings, ['censor_reason_bio_link'], reason_text))
                    except: pass
                else:
                    if redis_client: await redis_client.set(f"bio_status:{user_id}", "safe", ex=1600)
                    
            except Exception:
                pass
            finally:
                # কাজ শেষ হলে ইউজার লক রিলিজ করা
                redis_client = app.bot_data.get('redis_clients')[0] if app.bot_data.get('redis_clients') else None
                if redis_client: await redis_client.delete(f"bio_lock:{user_id}")
            
            app.bot_data['bio_queue'].task_done()
        except Exception as e:
            logger.error(f"Bio Task Error: {e}")

# --- ৪. জয়েন রিকোয়েস্ট হ্যান্ডেলার ---
async def handle_join_request(update: Update, context: ContextTypes.DEFAULT_TYPE):
    req = update.chat_join_request
    user, chat = req.from_user, req.chat
    pool = context.bot_data.get('db_pool')
    
    if user.username:
        clean_username = user.username.lower().replace("@", "")
        clients = context.bot_data.get('redis_clients')
        if clients:
            try: await clients[0].set(f"join_cache:{clean_username}", user.id, ex=2592000) 
            except: pass

    if pool:
        try:
            temp_expiry = datetime.now(timezone.utc) + timedelta(minutes=10)
            async with pool.acquire() as conn:
                await conn.execute("""
                    INSERT INTO vip_trials (chat_id, user_id, expiry_date, joined_at, is_safe)
                    VALUES ($1, $2, $3, CURRENT_TIMESTAMP, TRUE)
                    ON CONFLICT (chat_id, user_id) DO UPDATE 
                    SET expiry_date = EXCLUDED.expiry_date, is_safe = TRUE
                """, chat.id, user.id, temp_expiry)
        except: pass

# --- ৫. কোর হ্যান্ডেলার র‍্যাপার ---
async def w_bot_status(u, c): from bot_handlers.core import handle_bot_status_change; await handle_bot_status_change(u, c)
async def w_member_status(u, c): from bot_handlers.core import handle_member_status_change; await handle_member_status_change(u, c)
async def w_msg(u, c): from bot_handlers.core import handle_message; await handle_message(u, c)
async def w_edit_msg(u, c): from bot_handlers.core import handle_edited_message; await handle_edited_message(u, c)
async def w_callback(u, c): from bot_handlers import callbacks; await callbacks.w_callback(u, c)

async def safe_start_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
    user = update.effective_user
    if update.effective_chat.type != 'private': return
    pool = context.bot_data.get('db_pool')
    if pool:
        asyncio.create_task(pool.execute("""
            INSERT INTO users (user_id, first_name, username, has_started_bot, last_active_on) 
            VALUES ($1, $2, $3, TRUE, CURRENT_TIMESTAMP)
            ON CONFLICT (user_id) DO UPDATE SET has_started_bot = TRUE
        """, user.id, user.first_name, user.username))
    
    keyboard = InlineKeyboardMarkup([
        [InlineKeyboardButton("English 🇬🇧", callback_data='lang_en'), 
         InlineKeyboardButton("বাংলা 🇧🇩", callback_data='lang_bn')], 
        [InlineKeyboardButton("हिन्दी 🇮🇳", callback_data='lang_hi')]
    ])
    await update.message.reply_html(f"👋 <b>Hello {user.first_name}!</b>\n\nPlease choose language:", reply_markup=keyboard)

# --- ৬. POST INIT & STOP ---
async def post_init(app: Application):
    app.bot_data['db_pool'] = await create_db_pool_with_retry()
    await setup_database(app.bot_data['db_pool'])
    
    app.bot_data['redis_clients'] = []
    redis_urls = [REDIS_URL_1, REDIS_URL_2]
    for i, url in enumerate(redis_urls, 1):
        if url:
            try:
                r = redis.Redis.from_url(url, decode_responses=True); await r.ping()
                app.bot_data['redis_clients'].append(r)
                logger.info(f"✅ Redis {i} Connected!")
            except: pass

    app.bot_data['api_semaphore'] = asyncio.Semaphore(TELEGRAM_API_CONCURRENCY_LIMIT)
    app.bot_data['ocr_semaphore'] = asyncio.Semaphore(2)
    app.bot_data['batch_deleter'] = BatchDeleter(app.bot)
    app.bot_data['proc_pool'] = ThreadPoolExecutor(max_workers=4)
    app.bot_data['BOT_OWNER_ID'] = int(BOT_OWNER_ID) if BOT_OWNER_ID else None
    app.bot_data['REPORT_CHANNEL_ID'] = int(REPORT_CHANNEL_ID) if REPORT_CHANNEL_ID else None
    
    # 🔥 কিউ তৈরি এবং ব্যাকগ্রাউন্ড টাস্ক চালু করা
    app.bot_data['bio_queue'] = asyncio.Queue()
    asyncio.create_task(background_bio_task(app))
    
    # বাকি লুপগুলো শুরু
    asyncio.create_task(check_expired_trials_loop(app))
    asyncio.create_task(user_stats_flush_loop(app))
    asyncio.create_task(recovery_traffic_locks(app))
    
async def post_stop(app: Application):
    await db.flush_user_stats_to_db(app)
    if p := app.bot_data.get('db_pool'): await p.close()
    for r in app.bot_data.get('redis_clients', []): await r.close()

# --- ৭. প্রধান ফাংশন ---
def main():
    if not BOT_TOKEN: return
    app = Application.builder().token(BOT_TOKEN).post_init(post_init).post_stop(post_stop).build()
    app.add_error_handler(global_error_handler)
    
    from bot_handlers import admin_commands as ac
    from bot_handlers import extra_commands as ec
    from bot_handlers import owner_commands as oc

    app.add_handler(CommandHandler("start", safe_start_command))
    app.add_handler(CommandHandler("grant", ac.vip_trial_command))
    app.add_handler(CommandHandler("whois", ac.whois_command))
    app.add_handler(CommandHandler("whisper", ac.whisper_command))
    app.add_handler(CommandHandler("setwarn", ac.setwarn_command))
    app.add_handler(CommandHandler("addword", ac.add_word_command))
    app.add_handler(CommandHandler("delword", ac.del_word_command))
    app.add_handler(CommandHandler("wordlist", ac.wordlist_command))
    app.add_handler(CommandHandler("resetsettings", ac.reset_settings_command))
    app.add_handler(CommandHandler("filter", ec.filter_command))
    app.add_handler(CommandHandler("stop", ec.stop_filter_command))
    app.add_handler(CommandHandler("save", ec.save_note_command))
    app.add_handler(CommandHandler("setwelcome", ec.set_welcome_command))
    app.add_handler(CommandHandler("welcomecard", ec.welcome_card_command))
    app.add_handler(CommandHandler("shadow", ec.shadow_command))
    app.add_handler(CommandHandler("clean_ghosts", ec.clean_ghosts_command))
    app.add_handler(CommandHandler("linktomute", ec.linktomute_command))
    app.add_handler(CommandHandler("spamscan", ec.spamscan_command))
    app.add_handler(CommandHandler("antiduplicate", ec.antiduplicate_command))
    app.add_handler(CommandHandler("blockforwards", ec.blockforwards_command))
    app.add_handler(CommandHandler("blockbiolink", ec.blockbiolink_command))
    app.add_handler(CommandHandler("ocrscan", ec.ocrscan_command))
    app.add_handler(CommandHandler("traffic", ec.traffic_command))
    app.add_handler(CommandHandler("manageadmins", ec.manageadmins_command))
    app.add_handler(CommandHandler("allowusernames", ec.allowusernames_command))
    app.add_handler(CommandHandler("maxlength", ec.maxlength_command))
    app.add_handler(CommandHandler("autodelete", ec.autodelete_command))
    app.add_handler(CommandHandler("sentiment", ec.sentiment_command))
    app.add_handler(CommandHandler("ticket", ec.ticket_command))
    app.add_handler(CommandHandler("trustscore", ec.trustscore_command))
    app.add_handler(CommandHandler("fixdb", ec.fix_database_command))
    app.add_handler(CommandHandler("broadcast", oc.broadcast_command))
    app.add_handler(CommandHandler("bc", oc.broadcast_groups_command))
    app.add_handler(CommandHandler("stats", oc.stats_command))
    app.add_handler(CommandHandler("activity", oc.check_activity_command))
    app.add_handler(CommandHandler("get_dead_groups", oc.get_dead_groups_command))
    app.add_handler(CommandHandler("purge_dead_groups", oc.purge_dead_groups_command))
    app.add_handler(CommandHandler("backup", oc.backup_database_command))
    app.add_handler(CommandHandler("sban", oc.sban_command))
    app.add_handler(CommandHandler("sdel", oc.sdel_command))
    app.add_handler(CommandHandler("smute", oc.smute_command))

    app.add_handler(CallbackQueryHandler(w_callback))
    app.add_handler(ChatJoinRequestHandler(handle_join_request))
    app.add_handler(MessageHandler(filters.ALL & ~filters.COMMAND, w_msg))
    app.add_handler(MessageHandler(filters.UpdateType.EDITED_MESSAGE, w_edit_msg))
    app.add_handler(ChatMemberHandler(w_bot_status, ChatMemberHandler.MY_CHAT_MEMBER))
    app.add_handler(ChatMemberHandler(w_member_status, ChatMemberHandler.CHAT_MEMBER))

    threading.Thread(target=run_web_server, daemon=True).start()
    logger.info("Bot is polling silently...")
    app.run_polling(allowed_updates=["message", "callback_query", "chat_member", "my_chat_member", "chat_join_request", "edited_message"])

if __name__ == '__main__':
    main()