File size: 13,696 Bytes
2c41c5f e581a8c 2c41c5f e581a8c 07f63a8 2c41c5f e581a8c 2c41c5f 07f63a8 2c41c5f e581a8c 07f63a8 e581a8c 2c41c5f e581a8c 2c41c5f e581a8c 2c41c5f e581a8c 0759ec4 e581a8c 0759ec4 2c41c5f 0759ec4 2c41c5f e581a8c 2c41c5f e581a8c 0759ec4 2c41c5f 0759ec4 2c41c5f e581a8c 2c41c5f e581a8c 2c41c5f e581a8c 07f63a8 e581a8c 2c41c5f e581a8c 2c41c5f e581a8c 2c41c5f e581a8c 2c41c5f e581a8c 2c41c5f e581a8c 2c41c5f e581a8c 2c41c5f e581a8c 2c41c5f e581a8c 2c41c5f e581a8c 2c41c5f e581a8c 2c41c5f e581a8c 2c41c5f e581a8c 2c41c5f e581a8c 2c41c5f e581a8c 2c41c5f e581a8c 2c41c5f e581a8c 2c41c5f da0d638 2c41c5f da0d638 2c41c5f e581a8c 2c41c5f da0d638 2c41c5f e581a8c 2c41c5f e581a8c 07f63a8 e581a8c 2c41c5f e581a8c 2c41c5f e581a8c 2c41c5f e581a8c 2c41c5f e581a8c 2c41c5f e581a8c | 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 306 307 | from datetime import datetime, timedelta
from pyrogram import Client, filters
from pyrogram.types import (
CallbackQuery,
ForceReply,
InlineKeyboardButton,
InlineKeyboardMarkup,
Message,
)
from Backend import db
from Backend.config import Telegram
from Backend.helper.settings_manager import SettingsManager
from Backend.logger import LOGGER
#----- Configured approvers, falling back to the owner
def _approver_ids() -> list:
return SettingsManager.current().approver_ids or [Telegram.OWNER_ID]
#----- Resolve a target user's mention and username string for admin captions
async def _resolve_target_info(client: Client, target_user_id: int):
try:
target_user = await client.get_users(target_user_id)
return target_user.mention, (f"@{target_user.username}" if target_user.username else "N/A")
except Exception:
return f"User {target_user_id}", "N/A"
#----- Formatted plan/user block shared by approve and reject captions
def _plan_info_text(mention: str, username_str: str, user_id: int, duration, price) -> str:
return (
f"π€ <b>User:</b> {mention}\n"
f"π <b>User ID:</b> <code>{user_id}</code>\n"
f"π <b>Username:</b> {username_str}\n\n"
f"π¦ <b>Plan:</b> {duration} days (βΉ{price})"
)
#----- Update the acting admin's caption plus every other admin's copy
async def _apply_admin_captions(client: Client, callback_query: CallbackQuery, admin_messages: list, status_caption: str):
await callback_query.message.edit_caption(status_caption)
acting_msg_id = callback_query.message.id
for am in admin_messages:
if am["message_id"] == acting_msg_id:
continue
try:
await client.edit_message_caption(chat_id=am["chat_id"], message_id=am["message_id"], caption=status_caption)
except Exception:
pass
#----- Plan button pressed: compute expiry, DM payment instructions, set pending state
@Client.on_callback_query(filters.regex(r"^plan_([a-fA-F0-9]{24})$"))
async def plan_selection(client: Client, callback_query: CallbackQuery):
if not SettingsManager.current().subscription:
return await callback_query.answer("Subscriptions are not enabled.", show_alert=True)
plan_id = callback_query.matches[0].group(1)
plans = await db.get_subscription_plans()
plan = next((p for p in plans if p["_id"] == plan_id), None)
if not plan:
return await callback_query.answer("Invalid plan.", show_alert=True)
duration = plan["days"]
await callback_query.answer()
user_id = callback_query.from_user.id if callback_query.from_user else callback_query.message.chat.id
first_name = callback_query.from_user.first_name if callback_query.from_user else callback_query.message.chat.title
username = callback_query.from_user.username if callback_query.from_user else callback_query.message.chat.username
await db.update_user_interaction(user_id, first_name, username)
user = await db.get_user(user_id)
now = datetime.utcnow()
current_expiry = user.get("subscription_expiry") if user else None
if current_expiry and current_expiry > now:
new_expiry = current_expiry + timedelta(days=int(duration))
else:
new_expiry = now + timedelta(days=int(duration))
expiry_str = new_expiry.strftime("%Y-%m-%d %H:%M UTC")
settings = SettingsManager.current()
payment_instructions = settings.payment_instructions
payment_qr_url = settings.payment_qr_url
text = (
f"<b>β
Plan Selected: {plan['days']} Days</b>\n\n"
f"<b>π° Price:</b> βΉ{plan['price']}\n"
f"<b>π
Expiry (if approved now):</b> {expiry_str}\n\n"
f"<b>π How to Pay:</b>\n"
)
text += f"{payment_instructions}\n\n" if payment_instructions else f"Pay βΉ{plan['price']} to the admin.\n\n"
text += (
"<b>After paying:</b> send your payment screenshot directly here "
"(in this chat). The admin will review and activate your subscription."
)
await db.set_pending_payment(user_id, int(duration), 0, price=plan.get("price", 0))
#----- Prefer DMing the user so the private screenshot handler can pick it up
dm_sent = False
try:
if payment_qr_url:
try:
await client.send_photo(chat_id=user_id, photo=payment_qr_url, caption=f"π· Scan to pay βΉ{plan['price']}")
except Exception as qe:
LOGGER.warning(f"Could not send payment QR to {user_id}: {qe}")
await client.send_message(chat_id=user_id, text=text, reply_markup=ForceReply(selective=True))
dm_sent = True
except Exception as e:
LOGGER.warning(f"Could not DM user {user_id}: {e}")
if dm_sent:
await callback_query.answer("β
Check your DM for payment instructions!", show_alert=True)
else:
await callback_query.message.reply_text(
text + "\n\nβ οΈ <i>Please start a DM with the bot first by clicking its username, then send your screenshot there.</i>",
reply_markup=ForceReply(selective=True),
quote=True,
)
await callback_query.answer()
#----- Forward a user's payment screenshot to all approvers for review
@Client.on_message(filters.photo & filters.private)
async def handle_payment_screenshot(client: Client, message: Message):
if not SettingsManager.current().subscription:
return
sender_id = (message.from_user.id if message.from_user else None) \
or (message.sender_chat.id if message.sender_chat else None) \
or message.chat.id
try:
LOGGER.debug(f"handle_payment_screenshot triggered by {sender_id}")
user = await db.get_user(sender_id)
LOGGER.debug(f"user from DB = {user}")
if not user or "pending_payment" not in user:
LOGGER.debug(f"No pending_payment found for {sender_id}")
await message.reply_text(
"βΉοΈ We received your photo, but you don't have an active payment request.\n\n"
"Please use /start to select a subscription plan first, then send your payment screenshot.",
quote=True
)
return
pending = user["pending_payment"]
duration = pending.get("duration", "?")
price = pending.get("price", "?")
keyboard = InlineKeyboardMarkup([[
InlineKeyboardButton("β
Approve", callback_data=f"approve_{sender_id}"),
InlineKeyboardButton("β Reject", callback_data=f"reject_{sender_id}")
]])
user_mention = message.from_user.mention if message.from_user else f"User {sender_id}"
username_str = f"@{message.from_user.username}" if (message.from_user and message.from_user.username) else "N/A"
admin_text = (
f"<b>π° New Payment Screenshot Received</b>\n\n"
f"<b>π€ User:</b> {user_mention}\n"
f"<b>π User ID:</b> <code>{sender_id}</code>\n"
f"<b>π Username:</b> {username_str}\n\n"
f"<b>π¦ Plan Details:</b>\n"
f" β’ Duration: <b>{duration} days</b>\n"
f" β’ Price: <b>βΉ{price}</b>\n\n"
f"Please review the screenshot above and approve or reject."
)
admin_messages = []
for approver_id in _approver_ids():
try:
sent = await message.copy(approver_id, caption=admin_text, reply_markup=keyboard)
admin_messages.append({"chat_id": approver_id, "message_id": sent.id})
except Exception as e:
LOGGER.error(f"Failed to forward screenshot to approver {approver_id}: {e}")
await db.set_pending_payment(sender_id, duration, message.id, price=price, admin_messages=admin_messages)
if admin_messages:
await message.reply_text(
"β
<b>Screenshot Received!</b>\n\n"
"Your payment screenshot has been forwarded to the admin for review.\n"
"You will be notified once it's approved. Thank you! π",
quote=True
)
else:
await message.reply_text(
"β οΈ Your screenshot was received but we could not reach the admin. "
"Please contact the admin directly.",
quote=True
)
except Exception as e:
LOGGER.error(f"Error in handle_payment_screenshot: {e}")
await message.reply_text(
f"β οΈ Something went wrong while processing your screenshot. Please try again or contact the admin.\n\nError: {e}",
quote=True
)
#----- Approver taps Approve/Reject: update subscription and all admin captions
@Client.on_callback_query(filters.regex(r"^(approve|reject)_(\d+)$"))
async def admin_review(client: Client, callback_query: CallbackQuery):
if callback_query.from_user.id not in _approver_ids():
return await callback_query.answer("You are not authorized to perform this action.", show_alert=True)
action = callback_query.matches[0].group(1)
target_user_id = int(callback_query.matches[0].group(2))
acting_admin = callback_query.from_user
admin_name = acting_admin.first_name or acting_admin.username or f"Admin {acting_admin.id}"
#----- Read admin_messages before any DB write (approve/reject clears pending_payment)
user_pre = await db.get_user(target_user_id)
if not user_pre or "pending_payment" not in user_pre:
return await callback_query.answer("This request has already been processed.", show_alert=True)
admin_messages = user_pre["pending_payment"].get("admin_messages", [])
duration = user_pre["pending_payment"].get("duration", "?")
price = user_pre["pending_payment"].get("price", "?")
if action == "approve":
user_data = await db.approve_payment(target_user_id)
if not user_data:
return await callback_query.answer("Could not approve β no pending payment found.", show_alert=True)
try:
user_obj = await db.get_user(target_user_id)
user_name = (user_obj.get("first_name") or user_obj.get("username") or str(target_user_id)) if user_obj else str(target_user_id)
token_doc = await db.add_api_token(name=user_name, user_id=target_user_id)
addon_url = f"{SettingsManager.current().base_url}/stremio/{token_doc.get('token')}/manifest.json"
except Exception:
addon_url = None
try:
invite_link = await client.create_chat_invite_link(
chat_id=SettingsManager.current().subscription_group_id,
member_limit=1,
expire_date=datetime.utcnow() + timedelta(days=1)
)
invite_text = f"\n\nπ <b>Group Invite:</b> {invite_link.invite_link}"
except Exception:
invite_text = ""
expiry_str = user_data["subscription_expiry"].strftime("%Y-%m-%d")
success_text = (
f"π <b>Payment Approved!</b>\n\n"
f"Your subscription is now active until <b>{expiry_str}</b>."
f"{invite_text}"
)
if addon_url:
success_text += (
f"\n\nπ¬ <b>Stremio Addon β Install Link:</b>\n"
f"<code>{addon_url}</code>\n\n"
f"Tap the link above β <b>Install</b> in Stremio to start watching!"
)
await client.send_message(target_user_id, success_text)
mention, username_str = await _resolve_target_info(client, target_user_id)
info_text = _plan_info_text(mention, username_str, target_user_id, duration, price)
await _apply_admin_captions(client, callback_query, admin_messages, f"β
<b>Approved by {admin_name}</b>\n\n{info_text}")
elif action == "reject":
if not await db.reject_payment(target_user_id):
return await callback_query.answer("Could not reject β no pending payment found.", show_alert=True)
await client.send_message(
target_user_id,
"β <b>Payment Rejected</b>\n\nYour recent payment submission was rejected by the admin. Please contact the admin or try submitting again."
)
mention, username_str = await _resolve_target_info(client, target_user_id)
info_text = _plan_info_text(mention, username_str, target_user_id, duration, price)
await _apply_admin_captions(client, callback_query, admin_messages, f"β <b>Rejected by {admin_name}</b>\n\n{info_text}")
#----- /status: report the caller's active subscription and time remaining
@Client.on_message(filters.command("status"))
async def check_status(client: Client, message: Message):
if not SettingsManager.current().subscription:
return
user_id = (message.from_user.id if message.from_user else None) or (message.sender_chat.id if message.sender_chat else None) or message.chat.id
user = await db.get_user(user_id)
if not user or user.get("subscription_status") != "active":
return await message.reply_text("You do not have an active subscription.")
expiry = user.get("subscription_expiry")
if not expiry:
return await message.reply_text("Error retrieving expiry date.")
now = datetime.utcnow()
if now > expiry:
return await message.reply_text("Your subscription has expired.")
remaining = expiry - now
days = remaining.days
hours = remaining.seconds // 3600
await message.reply_text(
f"<b>Subscription Status:</b> Active β
\n"
f"<b>Expiry Date:</b> {expiry.strftime('%Y-%m-%d %H:%M UTC')}\n"
f"<b>Time Remaining:</b> {days} days and {hours} hours"
)
|