# handlers/share_channel.py from datetime import datetime, timedelta from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup from telegram.ext import ( ContextTypes, ConversationHandler, CommandHandler, CallbackQueryHandler ) from sqlalchemy import select from db import AsyncSessionLocal from models import Channel, ChannelAccess, SCOPE_POST from .permissions import get_or_create_admin, make_token, token_hash from telegram.error import BadRequest import os from telegram import InlineKeyboardMarkup, InlineKeyboardButton async def _get_bot_username(context: ContextTypes.DEFAULT_TYPE) -> str: # кэшируем, чтобы не дёргать get_me() каждый раз uname = context.application.bot.username if uname: return uname me = await context.bot.get_me() return me.username SELECT_CHANNEL, CONFIRM_INVITE = range(2) async def share_channel_start(update: Update, context: ContextTypes.DEFAULT_TYPE): async with AsyncSessionLocal() as session: me = await get_or_create_admin(session, update.effective_user.id) q = await session.execute(select(Channel).where(Channel.admin_id == me.id)) channels = q.scalars().all() if not channels: if update.message: await update.message.reply_text("Нет каналов, которыми вы владеете.") return ConversationHandler.END kb = [[InlineKeyboardButton(f"{c.name} ({c.link})", callback_data=f"sch_{c.id}")] for c in channels] rm = InlineKeyboardMarkup(kb) if update.message: await update.message.reply_text("Выберите канал для выдачи доступа:", reply_markup=rm) return SELECT_CHANNEL async def select_channel(update: Update, context: ContextTypes.DEFAULT_TYPE): q = update.callback_query if not q: return ConversationHandler.END await q.answer() if not q.data.startswith("sch_"): return ConversationHandler.END channel_id = int(q.data.split("_")[1]) context.user_data["share_channel_id"] = channel_id kb = [ [InlineKeyboardButton("Срок: 7 дней", callback_data="ttl_7"), InlineKeyboardButton("30 дней", callback_data="ttl_30"), InlineKeyboardButton("∞", callback_data="ttl_inf")], [InlineKeyboardButton("Выдать право постинга", callback_data="scope_post")], [InlineKeyboardButton("Сгенерировать ссылку", callback_data="go")], ] await q.edit_message_text("Настройте приглашение:", reply_markup=InlineKeyboardMarkup(kb)) context.user_data["ttl_days"] = 7 context.user_data["scopes"] = SCOPE_POST return CONFIRM_INVITE async def confirm_invite(update: Update, context: ContextTypes.DEFAULT_TYPE): q = update.callback_query if not q: return ConversationHandler.END # Лёгкий ACK, чтобы исчез «часик» на кнопке await q.answer() data = q.data # --- настройки TTL (ничего не меняем в разметке, только сохраняем выбор) --- if data.startswith("ttl_"): context.user_data["ttl_days"] = {"ttl_7": 7, "ttl_30": 30, "ttl_inf": None}[data] # Нечего редактировать — markup не менялся. Просто остаёмся в состоянии. return CONFIRM_INVITE # --- права: сейчас фиксировано SCOPE_POST, разметку не меняем --- if data == "scope_post": # если позже сделаешь тумблеры прав — тут можно перестраивать клавиатуру context.user_data["scopes"] = SCOPE_POST return CONFIRM_INVITE # --- генерация ссылки приглашения --- if data != "go": return CONFIRM_INVITE channel_id = context.user_data.get("share_channel_id") ttl_days = context.user_data.get("ttl_days") scopes = context.user_data.get("scopes", SCOPE_POST) async with AsyncSessionLocal() as session: me = await get_or_create_admin(session, update.effective_user.id) token = make_token(9) thash = token_hash(token) expires_at = None if ttl_days: from datetime import datetime, timedelta expires_at = datetime.utcnow() + timedelta(days=ttl_days) acc = ChannelAccess( channel_id=channel_id, invited_by_admin_id=me.id, token_hash=thash, scopes=scopes, status="pending", created_at=datetime.utcnow(), expires_at=expires_at, ) session.add(acc) await session.commit() invite_id = acc.id payload = f"sch_{invite_id}_{token}" bot_username = await _get_bot_username(context) deep_link = f"https://t.me/{bot_username}?start={payload}" # Кнопка для удобства kb = InlineKeyboardMarkup([[InlineKeyboardButton("Открыть ссылку", url=deep_link)]]) await q.edit_message_text( "Ссылка для предоставления доступа к каналу:\n" f"`{deep_link}`\n\n" "Передайте её коллеге. Срок действия — " + ("не ограничен." if ttl_days is None else f"{ttl_days} дней."), parse_mode="Markdown", reply_markup=kb, ) return ConversationHandler.END share_channel_conv = ConversationHandler( entry_points=[CommandHandler("share_channel", share_channel_start)], states={ SELECT_CHANNEL: [CallbackQueryHandler(select_channel, pattern="^sch_")], CONFIRM_INVITE: [CallbackQueryHandler(confirm_invite)], }, fallbacks=[], )