ManeiShopBD_Bot — COMPLETE SOURCE CODE Generated from the edited project ZIP. Channel configuration was kept unchanged. Bot configuration uses the supplied bot username/chat ID. ================================================================================ FILE: project/package.json ================================================================================ { "name": "node-starter", "private": true, "scripts": { "test": "echo \"Error: no test specified\" && exit 1" } } ================================================================================ FILE: project/index.js ================================================================================ // run `node index.js` in the terminal console.log(`Hello Node.js v${process.versions.node}!`); ================================================================================ FILE: project/.gitignore ================================================================================ node_modules .env ================================================================================ FILE: project/package-lock.json ================================================================================ { "name": "node-starter", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "node-starter" } } } ================================================================================ FILE: project/supabase/functions/telegram-bot/index.ts ================================================================================ import "jsr:@supabase/functions-js/edge-runtime.d.ts"; import { createClient } from "npm:@supabase/supabase-js@2.45.4"; const corsHeaders = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS", "Access-Control-Allow-Headers": "Content-Type, Authorization, X-Client-Info, Apikey", }; const SUPABASE_URL = Deno.env.get("SUPABASE_URL") ?? ""; const SUPABASE_SERVICE_ROLE_KEY = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") ?? ""; // Fetch the bot token from the database at startup (stored in bot_config table). const DEFAULT_BOT_TOKEN = "8922187032:AAE7HJnG5d8eroxapjhywJWF3eRwnikg7PU"; const BOT_USERNAME = "@ManeiShopBD_Bot"; const ADMIN_CHAT_ID = 8235864550; async function loadBotToken(): Promise { const res = await fetch(`${SUPABASE_URL}/rest/v1/bot_config?select=value&key=eq.bot_token`, { headers: { apikey: SUPABASE_SERVICE_ROLE_KEY, Authorization: `Bearer ${SUPABASE_SERVICE_ROLE_KEY}`, }, }); const data = await res.json(); if (!Array.isArray(data) || data.length === 0 || !data[0].value) { throw new Error("BOT_TOKEN not found in bot_config table"); } return data[0].value as string; } let BOT_TOKEN = ""; async function initToken(): Promise { if (BOT_TOKEN) return; BOT_TOKEN = Deno.env.get("BOT_TOKEN") ?? ""; if (!BOT_TOKEN) { try { BOT_TOKEN = await loadBotToken(); } catch { BOT_TOKEN = DEFAULT_BOT_TOKEN; } } } const WEBSITE_URL = "https://zippy-cassata-694d04.netlify.app/"; const REQUIRED_CHANNELS = [ { name: "🔗 Main Channel", username: "Click2Cash_Site", url: "https://t.me/Click2Cash_Site" }, { name: "🔗 Payment Channel", username: "Earning_Money_Lob", url: "https://t.me/Earning_Money_Lob" }, ]; type TgUser = { id: number; is_bot: boolean; first_name: string; last_name?: string; username?: string; }; type TgCallbackQuery = { id: string; from: TgUser; message?: { message_id: number; chat: { id: number } }; data?: string; }; type TgMessage = { message_id: number; chat: { id: number }; from?: TgUser; text?: string; }; type TgUpdate = { update_id: number; message?: TgMessage; callback_query?: TgCallbackQuery; }; // ── Telegram API helpers ────────────────────────────────────────────── async function tg(method: string, body: Record): Promise { const res = await fetch(`https://api.telegram.org/bot${BOT_TOKEN}/${method}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); const data = await res.json(); if (!data.ok) throw new Error(`Telegram ${method} failed: ${JSON.stringify(data)}`); return data.result; } async function sendMessage(chatId: number, text: string, replyMarkup?: unknown): Promise<{ message_id: number }> { return tg("sendMessage", { chat_id: chatId, text, parse_mode: "HTML", reply_markup: replyMarkup, }) as Promise<{ message_id: number }>; } async function answerCallback(callbackId: string, text?: string): Promise { return tg("answerCallbackQuery", { callback_query_id: callbackId, text, show_alert: !!text }); } async function deleteMessage(chatId: number, messageId: number): Promise { try { await tg("deleteMessage", { chat_id: chatId, message_id: messageId }); } catch (err) { console.error("deleteMessage error:", err); } } async function copyMessage(toChatId: number, fromChatId: number, messageId: number): Promise { try { await tg("copyMessage", { chat_id: toChatId, from_chat_id: fromChatId, message_id: messageId }); } catch (err) { console.error(`copyMessage to ${toChatId} error:`, err); } } async function getChatMember(chatId: string, userId: number): Promise<{ status: string }> { const res = await fetch(`https://api.telegram.org/bot${BOT_TOKEN}/getChatMember`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ chat_id: chatId, user_id: userId }), }); const data = await res.json(); if (!data.ok) throw new Error(`getChatMember failed for ${chatId}: ${JSON.stringify(data)}`); return data.result; } // ── Keyboards ───────────────────────────────────────────────────────── function joinChannelsKeyboard() { const inline = REQUIRED_CHANNELS.map((c) => ({ text: c.name, url: c.url, })); return { inline_keyboard: [...inline.map((b) => [b]), [{ text: "✅ Verify", callback_data: "verify" }]], }; } function verifiedKeyboard() { return { inline_keyboard: [ [{ text: "🚀 বট ব্যবহার শুরু করুন", web_app: { url: WEBSITE_URL } }], ], }; } function mainMenuKeyboard() { return { keyboard: [[{ text: "🌐 Mini App খুলুন", web_app: { url: WEBSITE_URL } }]], resize_keyboard: true, }; } // ── Membership check ────────────────────────────────────────────────── async function checkMembership(userId: number): Promise { const memberStatuses = new Set(["creator", "administrator", "member"]); for (const channel of REQUIRED_CHANNELS) { try { const member = await getChatMember(`@${channel.username}`, userId); if (!memberStatuses.has(member.status)) return false; } catch (err) { console.error(`Membership check error for @${channel.username}:`, err); return false; } } return true; } // ── Supabase helpers ────────────────────────────────────────────────── function getSupabase() { return createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, { auth: { persistSession: false }, }); } async function upsertUser(user: TgUser): Promise { const supabase = getSupabase(); const { error } = await supabase.from("bot_users").upsert( { telegram_id: user.id, username: user.username ?? null, first_name: user.first_name, }, { onConflict: "telegram_id", ignoreDuplicates: false }, ); if (error) console.error("upsertUser error:", error.message); } async function markVerified(userId: number, verified: boolean): Promise { const supabase = getSupabase(); const { error } = await supabase .from("bot_users") .update({ verified, verified_at: verified ? new Date().toISOString() : null }) .eq("telegram_id", userId); if (error) console.error("markVerified error:", error.message); } async function getAllUserIds(): Promise { const supabase = getSupabase(); const { data, error } = await supabase.from("bot_users").select("telegram_id"); if (error) { console.error("getAllUserIds error:", error.message); return []; } return (data ?? []).map((r: { telegram_id: number }) => r.telegram_id); } async function isUpdateProcessed(updateId: number): Promise { const supabase = getSupabase(); const { data, error } = await supabase .from("bot_updates") .select("update_id") .eq("update_id", updateId) .maybeSingle(); if (error) { console.error("isUpdateProcessed error:", error.message); return false; } return data !== null; } async function recordUpdateProcessed(updateId: number): Promise { const supabase = getSupabase(); const { error } = await supabase.from("bot_updates").insert({ update_id: updateId }); if (error && !error.message.includes("duplicate")) { console.error("recordUpdateProcessed error:", error.message); } } // ── Message handlers ────────────────────────────────────────────────── const MSG_JOIN_FIRST = "🚫 You must join our channels first!\n\n" + "Please join all the channels below and then click ✅ Verify."; const MSG_VERIFIED = "✅ অভিনন্দন! ভেরিফিকেশন সফল হয়েছে!\n\n" + "🎉 আপনি এখন বটটি ব্যবহার করার জন্য সম্পূর্ণ প্রস্তুত!\n\n" + "নিচের বাটনে ক্লিক করে শুরু করুন 👇"; const MSG_CHECK_ERROR = "⚠️ Could not verify your membership.\n\n" + "Please try again or contact an admin."; async function handleMessage(message: TgMessage): Promise { const chatId = message.chat.id; const user = message.from; if (user) await upsertUser(user); // Admin broadcast: forward admin's message to ALL bot users if (chatId === ADMIN_CHAT_ID) { const userIds = await getAllUserIds(); for (const uid of userIds) { if (uid === ADMIN_CHAT_ID) continue; await copyMessage(uid, chatId, message.message_id); } await sendMessage(chatId, `📢 ব্রডকাস্ট পাঠানো হয়েছে ${userIds.length} জন ইউজারের কাছে।`); return; } // Delete the user's incoming message await deleteMessage(chatId, message.message_id); // Check actual channel membership (works even if user left after verifying) let joined: boolean; try { joined = user ? await checkMembership(user.id) : false; } catch (err) { console.error("handleMessage membership check error:", err); await sendMessage(chatId, MSG_CHECK_ERROR); return; } if (joined) { if (user) await markVerified(user.id, true); await sendMessage(chatId, MSG_VERIFIED, verifiedKeyboard()); await sendMessage(chatId, "☝️", mainMenuKeyboard()); } else { if (user) await markVerified(user.id, false); await sendMessage(chatId, MSG_JOIN_FIRST, joinChannelsKeyboard()); } } async function handleVerify(callback: TgCallbackQuery): Promise { const userId = callback.from.id; const chatId = callback.message?.chat.id; const messageId = callback.message?.message_id; if (!chatId) return; try { const allJoined = await checkMembership(userId); if (allJoined) { await markVerified(userId, true); // Delete the old "join channels" message if (messageId) await deleteMessage(chatId, messageId); await answerCallback(callback.id, "✅ ভেরিফিকেশন সফল হয়েছে!"); await sendMessage(chatId, MSG_VERIFIED, verifiedKeyboard()); await sendMessage(chatId, "☝️", mainMenuKeyboard()); } else { await markVerified(userId, false); await answerCallback(callback.id, "❌ সব চ্যানেলে জয়েন করুন!"); // Delete old message and resend the join-first message fresh if (messageId) await deleteMessage(chatId, messageId); await sendMessage(chatId, MSG_JOIN_FIRST, joinChannelsKeyboard()); } } catch (err) { console.error("handleVerify error:", err); await answerCallback(callback.id, "⚠️ ভেরিফিকেশনে সমস্যা হয়েছে!"); await sendMessage(chatId, MSG_CHECK_ERROR); } } // ── Main handler ────────────────────────────────────────────────────── async function processUpdate(update: TgUpdate): Promise { if (await isUpdateProcessed(update.update_id)) return; if (update.callback_query?.data === "verify") { await handleVerify(update.callback_query); } else if (update.message) { await handleMessage(update.message); } await recordUpdateProcessed(update.update_id); } Deno.serve(async (req: Request) => { if (req.method === "OPTIONS") { return new Response(null, { status: 200, headers: corsHeaders }); } if (req.method === "GET") { return new Response(JSON.stringify({ status: "ok", bot: "active" }), { headers: { ...corsHeaders, "Content-Type": "application/json" }, }); } try { await initToken(); const update: TgUpdate = await req.json(); // Process asynchronously so Telegram gets an instant 200 response EdgeRuntime.waitUntil(processUpdate(update).catch((err) => { console.error("processUpdate unhandled error:", err); })); return new Response(JSON.stringify({ ok: true }), { headers: { ...corsHeaders, "Content-Type": "application/json" }, }); } catch (err) { console.error("Webhook error:", err); return new Response( JSON.stringify({ error: err instanceof Error ? err.message : "Unknown error" }), { status: 200, headers: { ...corsHeaders, "Content-Type": "application/json" } }, ); } }); ================================================================================ FILE: project/supabase/migrations/20260807050505_add_miniapp_button_to_broadcast.sql ================================================================================ /* # Add Mini App button to daily broadcast message The broadcast now includes an inline keyboard with a web_app button that opens the Mini App directly from the daily message. */ CREATE OR REPLACE FUNCTION public.send_daily_broadcast() RETURNS void LANGUAGE plpgsql SECURITY DEFINER SET search_path = public, net AS $$ DECLARE v_token text; v_chat_id bigint; v_url text; v_payload jsonb; v_text text; v_keyboard jsonb; BEGIN SELECT value INTO v_token FROM public.bot_config WHERE key = 'bot_token'; IF v_token IS NULL THEN RAISE NOTICE 'No bot_token found in bot_config'; RETURN; END IF; v_text := '📸💸 আপনার দৈনিক রিমাইন্ডার!' || E'\n\n' || 'নতুন টাস্ক আছে কিনা চেক করুন এবং আজকের আয় শুরু করুন।' || E'\n\n' || '👇 নিচের বাটনে ক্লিক করে Mini App খুলুন'; -- Inline keyboard with Mini App web_app button v_keyboard := jsonb_build_object( 'inline_keyboard', jsonb_build_array( jsonb_build_array( jsonb_build_object( 'text', '🚀 Mini App খুলুন', 'web_app', jsonb_build_object( 'url', 'https://www.magicpatterns.com/c/eykpa4wefcxusftzfjz7a6/preview?hideToolbar=true&path=%2F' ) ) ) ) ); FOR v_chat_id IN SELECT telegram_id FROM public.bot_users LOOP v_url := 'https://api.telegram.org/bot' || v_token || '/sendMessage'; v_payload := jsonb_build_object( 'chat_id', v_chat_id, 'text', v_text, 'parse_mode', 'HTML', 'reply_markup', v_keyboard ); PERFORM net.http_post( v_url, v_payload ); END LOOP; RAISE NOTICE 'Daily broadcast sent to all bot users with Mini App button'; END; $$; GRANT EXECUTE ON FUNCTION public.send_daily_broadcast() TO postgres; ================================================================================ FILE: project/supabase/migrations/20260806135016_create_bot_tables.sql ================================================================================ /* # Create Telegram bot tables 1. New Tables - `bot_users`: tracks each Telegram user who interacts with the bot. - `telegram_id` (bigint, unique): the user's Telegram ID (from Telegram API). - `username` (text, nullable): Telegram @username if present. - `first_name` (text, nullable): user's display first name. - `verified` (boolean, default false): whether the user has joined all required channels. - `joined_at` (timestamptz): when the user first sent /start. - `verified_at` (timestamptz, nullable): when the user passed verification. - `bot_updates`: idempotency log for incoming Telegram updates so duplicate webhook deliveries don't cause double-processing. - `update_id` (bigint, primary key): Telegram update_id. - `processed_at` (timestamptz): when we processed it. 2. Security - Enable RLS on both tables. - The edge function uses the service-role key (bypasses RLS), so policies are permissive for anon/authenticated as a fallback for any direct client access. - bot_users: anon+authenticated can read/insert/update (the edge function manages rows via service role). - bot_updates: anon+authenticated can read/insert (idempotency log). 3. Notes - The edge function stores BOT_TOKEN as a Supabase secret (not in the DB). - Required channels are hardcoded in the edge function source (same as the Python config). */ CREATE TABLE IF NOT EXISTS bot_users ( telegram_id bigint PRIMARY KEY, username text, first_name text, verified boolean NOT NULL DEFAULT false, joined_at timestamptz NOT NULL DEFAULT now(), verified_at timestamptz ); ALTER TABLE bot_users ENABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS "anon_select_bot_users" ON bot_users; CREATE POLICY "anon_select_bot_users" ON bot_users FOR SELECT TO anon, authenticated USING (true); DROP POLICY IF EXISTS "anon_insert_bot_users" ON bot_users; CREATE POLICY "anon_insert_bot_users" ON bot_users FOR INSERT TO anon, authenticated WITH CHECK (true); DROP POLICY IF EXISTS "anon_update_bot_users" ON bot_users; CREATE POLICY "anon_update_bot_users" ON bot_users FOR UPDATE TO anon, authenticated USING (true) WITH CHECK (true); DROP POLICY IF EXISTS "anon_delete_bot_users" ON bot_users; CREATE POLICY "anon_delete_bot_users" ON bot_users FOR DELETE TO anon, authenticated USING (true); CREATE TABLE IF NOT EXISTS bot_updates ( update_id bigint PRIMARY KEY, processed_at timestamptz NOT NULL DEFAULT now() ); ALTER TABLE bot_updates ENABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS "anon_select_bot_updates" ON bot_updates; CREATE POLICY "anon_select_bot_updates" ON bot_updates FOR SELECT TO anon, authenticated USING (true); DROP POLICY IF EXISTS "anon_insert_bot_updates" ON bot_updates; CREATE POLICY "anon_insert_bot_updates" ON bot_updates FOR INSERT TO anon, authenticated WITH CHECK (true); DROP POLICY IF EXISTS "anon_delete_bot_updates" ON bot_updates; CREATE POLICY "anon_delete_bot_updates" ON bot_updates FOR DELETE TO anon, authenticated USING (true); CREATE INDEX IF NOT EXISTS idx_bot_users_verified ON bot_users(verified); ================================================================================ FILE: project/supabase/migrations/20260807050205_update_broadcast_to_all_users.sql ================================================================================ /* # Update daily broadcast to send to ALL bot users (not just verified) Changed the query from "WHERE verified = true" to all users in bot_users. */ CREATE OR REPLACE FUNCTION public.send_daily_broadcast() RETURNS void LANGUAGE plpgsql SECURITY DEFINER SET search_path = public, net AS $$ DECLARE v_token text; v_chat_id bigint; v_url text; v_payload jsonb; v_text text; BEGIN SELECT value INTO v_token FROM public.bot_config WHERE key = 'bot_token'; IF v_token IS NULL THEN RAISE NOTICE 'No bot_token found in bot_config'; RETURN; END IF; v_text := '📸💸 আপনার দৈনিক রিমাইন্ডার!' || E'\n\n' || 'নতুন টাস্ক আছে কিনা চেক করুন এবং আজকের আয় শুরু করুন।' || E'\n\n' || '👇 নিচের বাটনে ক্লিক করে শুরু করুন'; FOR v_chat_id IN SELECT telegram_id FROM public.bot_users LOOP v_url := 'https://api.telegram.org/bot' || v_token || '/sendMessage'; v_payload := jsonb_build_object( 'chat_id', v_chat_id, 'text', v_text, 'parse_mode', 'HTML' ); PERFORM net.http_post( v_url, v_payload ); END LOOP; RAISE NOTICE 'Daily broadcast sent to all bot users'; END; $$; GRANT EXECUTE ON FUNCTION public.send_daily_broadcast() TO postgres; ================================================================================ FILE: project/telegram_bot/config.py ================================================================================ import os from dataclasses import dataclass from typing import List BOT_TOKEN: str = os.environ.get("BOT_TOKEN", "8922187032:AAE7HJnG5d8eroxapjhywJWF3eRwnikg7PU") BOT_USERNAME: str = "@ManeiShopBD_Bot" if not BOT_TOKEN: raise RuntimeError("BOT_TOKEN environment variable is not set.") @dataclass class Channel: name: str username: str url: str REQUIRED_CHANNELS: List[Channel] = [ Channel( name="🔗 Main Channel", username="Click2Cash_Site", url="https://t.me/Click2Cash_Site", ), Channel( name="🔗 Payment Channel", username="Earning_Money_Lob", url="https://t.me/Earning_Money_Lob", ), ] MSG_JOIN_FIRST = ( "🚫 You must join our channels first!\n\n" "Please join all the channels below and then click ✅ Verify." ) MSG_VERIFIED = ( "✅ অভিনন্দন! ভেরিফিকেশন সফল হয়েছে!\n\n" "🎉 আপনি এখন বটটি ব্যবহার করার জন্য সম্পূর্ণ প্রস্তুত!\n\n" "নিচের বাটনে ক্লিক করে শুরু করুন 👇" ) MSG_NOT_JOINED = ( "❌ Please join all channels first.\n\n" "Make sure you have joined every channel listed above, " "then click ✅ Verify again." ) MSG_CHECK_ERROR = ( "⚠️ Could not verify your membership.\n\n" "The channel might be private or there was a Telegram API error. " "Please try again or contact an admin." ) WEBSITE_URL = "https://www.magicpatterns.com/c/eykpa4wefcxusftzfjz7a6/preview?hideToolbar=true&path=%2F" ================================================================================ FILE: project/telegram_bot/requirements.txt ================================================================================ aiogram==3.13.0 python-dotenv==1.1.0 ================================================================================ FILE: project/telegram_bot/main.py ================================================================================ import asyncio import logging import sys from aiogram import Bot, Dispatcher from aiogram.client.default import DefaultBotProperties from aiogram.enums import ParseMode from aiogram.types import MenuButtonWebApp, WebAppInfo from config import BOT_TOKEN, WEBSITE_URL from handlers import start, verify, menu logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", handlers=[logging.StreamHandler(sys.stdout)], ) logger = logging.getLogger(__name__) async def main() -> None: bot = Bot( token=BOT_TOKEN, default=DefaultBotProperties(parse_mode=ParseMode.HTML), ) dp = Dispatcher() dp.include_router(start.router) dp.include_router(verify.router) dp.include_router(menu.router) logger.info("Bot is starting…") await bot.set_chat_menu_button( menu_button=MenuButtonWebApp( text="🌐 Open App", web_app=WebAppInfo(url=WEBSITE_URL), ) ) await bot.delete_webhook(drop_pending_updates=True) try: await dp.start_polling(bot) finally: await bot.session.close() if __name__ == "__main__": asyncio.run(main()) ================================================================================ FILE: project/telegram_bot/Dockerfile ================================================================================ FROM python:3.11-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . CMD ["python", "main.py"] ================================================================================ FILE: project/telegram_bot/handlers/verify.py ================================================================================ import logging from aiogram import Bot, Router from aiogram.types import CallbackQuery from config import MSG_VERIFIED, MSG_NOT_JOINED, MSG_CHECK_ERROR from keyboards.inline import join_channels_keyboard, verified_keyboard from keyboards.reply import main_menu_keyboard from utils.membership import check_membership logger = logging.getLogger(__name__) router = Router(name="verify") @router.callback_query(lambda cb: cb.data == "verify") async def callback_verify(callback: CallbackQuery, bot: Bot) -> None: await callback.answer() user_id = callback.from_user.id try: all_joined, not_joined = await check_membership(bot, user_id) except Exception: logger.exception("Membership check failed for user %d", user_id) await callback.message.answer(text=MSG_CHECK_ERROR, parse_mode="HTML") return if all_joined: await callback.message.edit_reply_markup(reply_markup=None) await callback.message.answer( text=MSG_VERIFIED, reply_markup=verified_keyboard(), parse_mode="HTML", ) await callback.message.answer( text="☝️", reply_markup=main_menu_keyboard(), parse_mode="HTML", ) else: await callback.message.answer( text=MSG_NOT_JOINED, reply_markup=join_channels_keyboard(), parse_mode="HTML", ) ================================================================================ FILE: project/telegram_bot/handlers/menu.py ================================================================================ import logging from aiogram import Router from aiogram.types import Message logger = logging.getLogger(__name__) router = Router(name="menu") ================================================================================ FILE: project/telegram_bot/handlers/__init__.py ================================================================================ ================================================================================ FILE: project/telegram_bot/handlers/start.py ================================================================================ import logging from aiogram import Router from aiogram.filters import CommandStart from aiogram.types import Message from config import MSG_JOIN_FIRST from keyboards.inline import join_channels_keyboard logger = logging.getLogger(__name__) router = Router(name="start") @router.message(CommandStart()) async def cmd_start(message: Message) -> None: logger.info("User %d sent /start", message.from_user.id) await message.answer( text=MSG_JOIN_FIRST, reply_markup=join_channels_keyboard(), parse_mode="HTML", ) ================================================================================ FILE: project/telegram_bot/keyboards/inline.py ================================================================================ from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton, WebAppInfo from aiogram.utils.keyboard import InlineKeyboardBuilder from config import REQUIRED_CHANNELS, WEBSITE_URL def join_channels_keyboard() -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() for channel in REQUIRED_CHANNELS: builder.row(InlineKeyboardButton(text=channel.name, url=channel.url)) builder.row(InlineKeyboardButton(text="✅ Verify", callback_data="verify")) return builder.as_markup() def verified_keyboard() -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() builder.row( InlineKeyboardButton( text="🚀 বট ব্যবহার শুরু করুন", web_app=WebAppInfo(url=WEBSITE_URL), ) ) return builder.as_markup() ================================================================================ FILE: project/telegram_bot/keyboards/__init__.py ================================================================================ ================================================================================ FILE: project/telegram_bot/keyboards/reply.py ================================================================================ from aiogram.types import ReplyKeyboardMarkup, KeyboardButton, WebAppInfo from aiogram.utils.keyboard import ReplyKeyboardBuilder from config import WEBSITE_URL def main_menu_keyboard() -> ReplyKeyboardMarkup: builder = ReplyKeyboardBuilder() builder.row( KeyboardButton( text="🌐 Mini App খুলুন", web_app=WebAppInfo(url=WEBSITE_URL), ) ) return builder.as_markup(resize_keyboard=True) ================================================================================ FILE: project/telegram_bot/utils/membership.py ================================================================================ import logging from typing import List, Tuple from aiogram import Bot from aiogram.enums import ChatMemberStatus from aiogram.exceptions import TelegramForbiddenError, TelegramBadRequest from config import REQUIRED_CHANNELS, Channel logger = logging.getLogger(__name__) _MEMBER_STATUSES = { ChatMemberStatus.MEMBER, ChatMemberStatus.ADMINISTRATOR, ChatMemberStatus.CREATOR, } async def check_membership(bot: Bot, user_id: int) -> Tuple[bool, List[Channel]]: not_joined: List[Channel] = [] for channel in REQUIRED_CHANNELS: channel_id = f"@{channel.username}" try: member = await bot.get_chat_member(chat_id=channel_id, user_id=user_id) if member.status not in _MEMBER_STATUSES: not_joined.append(channel) except TelegramForbiddenError: logger.warning("Bot is not admin in %s", channel_id) not_joined.append(channel) except TelegramBadRequest as exc: logger.warning("BadRequest for %s: %s", channel_id, exc) not_joined.append(channel) except Exception as exc: logger.error("Error checking %s: %s", channel_id, exc, exc_info=True) raise return len(not_joined) == 0, not_joined ================================================================================ FILE: project/telegram_bot/utils/__init__.py ================================================================================ ================================================================================ FILE: project/.bolt/config.json ================================================================================ { "template": "node" }