#!/usr/bin/env python3 """ Discord Sales & Support Bot — Ultimate Investment Tool ======================================================= Features: /setup → (Admin only) Posts the persistent sales embed with Purchase & Support buttons into the current channel. Run this once in your read-only sales channel. Purchase button → Stripe Checkout (weekly $10 / monthly $25 / lifetime $150) License key auto-generated + DM'd on successful payment Role assigned automatically in the server Support button → Modal prompts for a subject, then opens a private ticket channel with a Close button and audit log Setup: copy .env.example → .env and fill in all values before running. """ import asyncio import datetime import os import uuid import aiohttp from aiohttp import web import discord from discord import app_commands from discord.ext import commands import requests import stripe from dotenv import load_dotenv load_dotenv() # ───────────────────────────────────────────────────────────────────────────── # Configuration (all values from .env) # ───────────────────────────────────────────────────────────────────────────── DISCORD_TOKEN = os.getenv("DISCORD_TOKEN") GUILD_ID = int(os.getenv("GUILD_ID", "0")) STRIPE_SECRET_KEY = os.getenv("STRIPE_SECRET_KEY") STRIPE_WEBHOOK_SECRET = os.getenv("STRIPE_WEBHOOK_SECRET") SUPABASE_URL = os.getenv("SUPABASE_URL") SUPABASE_SERVICE_KEY = os.getenv("SUPABASE_SERVICE_ROLE_KEY") # Discord IDs — right-click channel/role → Copy ID (needs Developer Mode enabled) TICKET_CATEGORY_ID = int(os.getenv("TICKET_CATEGORY_ID", "0")) TICKET_LOG_CHANNEL_ID = int(os.getenv("TICKET_LOG_CHANNEL_ID", "0")) ROLE_USER_ID = int(os.getenv("ROLE_USER_ID", "0")) ROLE_LIFETIME_ID = int(os.getenv("ROLE_LIFETIME_ID", "0")) WEBHOOK_PORT = int(os.getenv("WEBHOOK_PORT", "8080")) # Pricing tiers PRICES = { "weekly": {"amount": 1000, "label": "$10 / week", "days": 7}, "monthly": {"amount": 2500, "label": "$25 / month", "days": 30}, "lifetime": {"amount": 15000, "label": "$150 lifetime", "days": None}, } stripe.api_key = STRIPE_SECRET_KEY _SUPABASE_HEADERS = { "apikey": SUPABASE_SERVICE_KEY, "Authorization": f"Bearer {SUPABASE_SERVICE_KEY}", "Content-Type": "application/json", "Prefer": "return=representation", } # ───────────────────────────────────────────────────────────────────────────── # License helpers # ───────────────────────────────────────────────────────────────────────────── def _generate_key() -> str: parts = [uuid.uuid4().hex[:8].upper() for _ in range(3)] return "UIT-" + "-".join(parts) def issue_license(tier: str, notes: str = "") -> str: """Create a new license in Supabase and return the key.""" days = PRICES[tier]["days"] expiry = None if days is not None: expiry = (datetime.datetime.utcnow() + datetime.timedelta(days=days)).isoformat() + "Z" key = _generate_key() payload = { "license_key": key, "tier": tier, "expiry_date": expiry, "active": True, "machines_allowed": 1, "notes": notes, } resp = requests.post( f"{SUPABASE_URL}/rest/v1/licenses", headers=_SUPABASE_HEADERS, json=payload, timeout=10, ) resp.raise_for_status() return key # ───────────────────────────────────────────────────────────────────────────── # Stripe helpers # ───────────────────────────────────────────────────────────────────────────── def create_checkout_session(tier: str, discord_user_id: str, discord_username: str) -> str: """Create a Stripe Checkout Session and return its URL.""" price_info = PRICES[tier] session = stripe.checkout.Session.create( payment_method_types=["card"], line_items=[{ "price_data": { "currency": "usd", "unit_amount": price_info["amount"], "product_data": { "name": f"Ultimate Investment Tool — {price_info['label']}", }, }, "quantity": 1, }], mode="payment", success_url=os.getenv("STRIPE_SUCCESS_URL", "https://discord.com/channels/@me"), cancel_url=os.getenv("STRIPE_CANCEL_URL", "https://discord.com/channels/@me"), metadata={ "discord_user_id": discord_user_id, "discord_username": discord_username, "tier": tier, }, ) return session.url # ───────────────────────────────────────────────────────────────────────────── # Bot setup # ───────────────────────────────────────────────────────────────────────────── intents = discord.Intents.default() intents.members = True bot = commands.Bot(command_prefix="!", intents=intents) tree = bot.tree # ───────────────────────────────────────────────────────────────────────────── # Ticket creation helper (shared by button and any future commands) # ───────────────────────────────────────────────────────────────────────────── async def create_ticket(interaction: discord.Interaction, subject: str): guild = interaction.guild category = guild.get_channel(TICKET_CATEGORY_ID) overwrites = { guild.default_role: discord.PermissionOverwrite(view_channel=False), interaction.user: discord.PermissionOverwrite( view_channel=True, send_messages=True, read_message_history=True, ), } for role in guild.roles: if role.permissions.administrator or role.permissions.manage_channels: overwrites[role] = discord.PermissionOverwrite( view_channel=True, send_messages=True, read_message_history=True, manage_channels=True, ) safe_name = "".join(c for c in interaction.user.name if c.isalnum() or c in "-_")[:20] channel_name = f"ticket-{safe_name}" if category: existing = discord.utils.get(category.text_channels, name=channel_name) if existing: await interaction.followup.send( f"You already have an open ticket: {existing.mention}\n" "Please use that channel or close it before opening a new one.", ephemeral=True, ) return channel = await guild.create_text_channel( name=channel_name, category=category, overwrites=overwrites, topic=f"Ticket by {interaction.user} ({interaction.user.id}) | {subject}", ) embed = discord.Embed( title=f"Support Ticket — {subject}", description=( f"Welcome {interaction.user.mention}!\n\n" "Please describe your issue in as much detail as possible.\n" "A staff member will be with you shortly.\n\n" "Press **🔒 Close Ticket** when your issue is resolved." ), color=discord.Color.blurple(), timestamp=discord.utils.utcnow(), ) embed.set_footer(text=str(interaction.user), icon_url=interaction.user.display_avatar.url) await channel.send(embed=embed, view=TicketCloseView()) log_channel = guild.get_channel(TICKET_LOG_CHANNEL_ID) if log_channel: log_embed = discord.Embed( title="Ticket Opened", description=( f"**User:** {interaction.user.mention} (`{interaction.user.id}`)\n" f"**Channel:** {channel.mention}\n" f"**Subject:** {subject}" ), color=discord.Color.green(), timestamp=discord.utils.utcnow(), ) await log_channel.send(embed=log_embed) await interaction.followup.send( f"Your ticket has been created: {channel.mention}", ephemeral=True ) # ───────────────────────────────────────────────────────────────────────────── # Persistent Views (survive bot restarts) # ───────────────────────────────────────────────────────────────────────────── class TicketCloseView(discord.ui.View): """Close button that lives permanently in ticket channels.""" def __init__(self): super().__init__(timeout=None) @discord.ui.button( label="🔒 Close Ticket", style=discord.ButtonStyle.danger, custom_id="ticket_close", ) async def close_ticket(self, interaction: discord.Interaction, button: discord.ui.Button): channel = interaction.channel guild = interaction.guild await interaction.response.send_message( f"Ticket closed by {interaction.user.mention}. Channel will be deleted in 5 seconds.", ephemeral=False, ) log_channel = guild.get_channel(TICKET_LOG_CHANNEL_ID) if log_channel: embed = discord.Embed( title="Ticket Closed", description=( f"**Channel:** {channel.name}\n" f"**Closed by:** {interaction.user.mention}\n" f"**Topic:** {channel.topic or 'N/A'}" ), color=discord.Color.red(), timestamp=discord.utils.utcnow(), ) await log_channel.send(embed=embed) await asyncio.sleep(5) await channel.delete(reason=f"Ticket closed by {interaction.user}") class TierSelectView(discord.ui.View): """Three buttons for selecting a pricing tier — shown ephemerally after clicking Purchase.""" def __init__(self): super().__init__(timeout=120) async def _handle(self, interaction: discord.Interaction, tier: str): await interaction.response.defer(ephemeral=True, thinking=True) try: url = create_checkout_session( tier, str(interaction.user.id), str(interaction.user), ) price_label = PRICES[tier]["label"] embed = discord.Embed( title="Complete Your Purchase", description=( f"You selected the **{price_label}** plan.\n\n" f"[**→ Pay securely via Stripe**]({url})\n\n" "Your license key will be **DM'd to you instantly** after payment.\n" "The checkout link is valid for **24 hours**." ), color=discord.Color.green(), ) embed.set_footer(text="Powered by Stripe — we never store your card details.") await interaction.followup.send(embed=embed, ephemeral=True) except Exception as exc: await interaction.followup.send( f"❌ Could not create a checkout session. Please try again later.\n`{exc}`", ephemeral=True, ) @discord.ui.button(label="$10 / Week", style=discord.ButtonStyle.primary, custom_id="buy_weekly") async def weekly(self, interaction: discord.Interaction, button: discord.ui.Button): await self._handle(interaction, "weekly") @discord.ui.button(label="$25 / Month", style=discord.ButtonStyle.primary, custom_id="buy_monthly") async def monthly(self, interaction: discord.Interaction, button: discord.ui.Button): await self._handle(interaction, "monthly") @discord.ui.button(label="$150 Lifetime", style=discord.ButtonStyle.success, custom_id="buy_lifetime") async def lifetime(self, interaction: discord.Interaction, button: discord.ui.Button): await self._handle(interaction, "lifetime") class TicketModal(discord.ui.Modal, title="Open a Support Ticket"): """Modal that collects a subject before creating the ticket channel.""" subject = discord.ui.TextInput( label="Subject", placeholder="Brief description of your issue", max_length=100, required=True, ) async def on_submit(self, interaction: discord.Interaction): await interaction.response.defer(ephemeral=True, thinking=True) await create_ticket(interaction, self.subject.value) class SalesView(discord.ui.View): """ Persistent view with Purchase and Support buttons. Posted once by /setup into the read-only sales channel. """ def __init__(self): super().__init__(timeout=None) @discord.ui.button( label="Purchase", style=discord.ButtonStyle.success, custom_id="sales_purchase", emoji="🛒", ) async def purchase(self, interaction: discord.Interaction, button: discord.ui.Button): embed = discord.Embed( title="Ultimate Investment Tool — Pricing", description=( "Select a tier below. Payment is handled securely via **Stripe**.\n" "Your license key will be delivered to your **DMs** the moment payment clears." ), color=discord.Color.gold(), ) embed.add_field(name="Weekly", value="**$10** / 7 days", inline=True) embed.add_field(name="Monthly", value="**$25** / 30 days", inline=True) embed.add_field(name="Lifetime", value="**$150** one-time", inline=True) embed.set_footer(text="Having trouble? Click the Support button to open a ticket.") await interaction.response.send_message(embed=embed, view=TierSelectView(), ephemeral=True) @discord.ui.button( label="Support", style=discord.ButtonStyle.secondary, custom_id="sales_support", emoji="🎫", ) async def support(self, interaction: discord.Interaction, button: discord.ui.Button): await interaction.response.send_modal(TicketModal()) # ───────────────────────────────────────────────────────────────────────────── # Admin slash command — post the sales embed # ───────────────────────────────────────────────────────────────────────────── @tree.command(name="setup", description="Post the sales & support embed (admin only)") @app_commands.checks.has_permissions(administrator=True) async def setup(interaction: discord.Interaction): embed = discord.Embed( title="Ultimate Investment Tool", description=( "Click on the **Purchase** button to purchase a license. " "Click on the **Support** button to create a support ticket. " "Support tickets are checked by our staff, and can take time to get a response. " "Please be patient when waiting for staff to respond." ), color=discord.Color.gold(), ) await interaction.channel.send(embed=embed, view=SalesView()) await interaction.response.send_message("Sales embed posted.", ephemeral=True) @setup.error async def setup_error(interaction: discord.Interaction, error: app_commands.AppCommandError): if isinstance(error, app_commands.MissingPermissions): await interaction.response.send_message( "You need Administrator permission to use this command.", ephemeral=True ) # ───────────────────────────────────────────────────────────────────────────── # On-ready # ───────────────────────────────────────────────────────────────────────────── @bot.event async def on_ready(): # Re-register all persistent views so buttons work after restarts bot.add_view(SalesView()) bot.add_view(TicketCloseView()) # Sync slash commands to the guild instantly (no 1-hour global wait) guild_obj = discord.Object(id=GUILD_ID) tree.copy_global_to(guild=guild_obj) await tree.sync(guild=guild_obj) print(f"[Bot] Online as {bot.user} | Guild {GUILD_ID} | Slash commands synced") # ───────────────────────────────────────────────────────────────────────────── # Stripe webhook (aiohttp server on WEBHOOK_PORT) # ───────────────────────────────────────────────────────────────────────────── async def _handle_stripe_webhook(request: web.Request) -> web.Response: payload = await request.read() sig_header = request.headers.get("stripe-signature", "") try: event = stripe.Webhook.construct_event(payload, sig_header, STRIPE_WEBHOOK_SECRET) except stripe.error.SignatureVerificationError: print("[Webhook] Invalid Stripe signature — request rejected") return web.Response(status=400, text="Invalid signature") except Exception as exc: print(f"[Webhook] Error parsing event: {exc}") return web.Response(status=400, text=str(exc)) if event["type"] == "checkout.session.completed": session = event["data"]["object"] metadata = session.get("metadata", {}) discord_user_id = metadata.get("discord_user_id") discord_username = metadata.get("discord_username", "Unknown") tier = metadata.get("tier") if not discord_user_id or tier not in PRICES: print(f"[Webhook] Missing/invalid metadata: {metadata}") return web.Response(status=200, text="OK") # 1. Issue license in Supabase try: key = issue_license( tier, notes=f"Discord: {discord_username} ({discord_user_id})", ) print(f"[License] Issued {key} ({tier}) for Discord user {discord_user_id}") except Exception as exc: print(f"[License] Failed to issue for {discord_user_id}: {exc}") return web.Response(status=500, text="License issuance failed") # 2. DM the license key to the buyer price_info = PRICES[tier] expiry_str = ( "Never (lifetime)" if price_info["days"] is None else f"{price_info['days']} days from today" ) try: user = await bot.fetch_user(int(discord_user_id)) embed = discord.Embed( title="🎉 Purchase Confirmed — Ultimate Investment Tool", description=( f"Thank you for your purchase!\n\n" f"**Your License Key:**\n```\n{key}\n```\n" f"**Plan:** {price_info['label']}\n" f"**Expires:** {expiry_str}\n\n" "Paste this key into the application when prompted.\n" "⚠️ This key is locked to one machine — keep it private." ), color=discord.Color.green(), ) embed.set_footer(text="Need help? Click the Support button in the server.") await user.send(embed=embed) print(f"[Bot] License DM'd to {discord_username} ({discord_user_id})") except discord.Forbidden: print(f"[Bot] Cannot DM {discord_user_id} — DMs may be disabled.") except Exception as exc: print(f"[Bot] DM error for {discord_user_id}: {exc}") # 3. Assign roles in the guild try: guild = bot.get_guild(GUILD_ID) if guild: member = await guild.fetch_member(int(discord_user_id)) roles_to_add = [] user_role = guild.get_role(ROLE_USER_ID) if user_role: roles_to_add.append(user_role) if tier == "lifetime": lifetime_role = guild.get_role(ROLE_LIFETIME_ID) if lifetime_role: roles_to_add.append(lifetime_role) if roles_to_add: await member.add_roles(*roles_to_add, reason=f"Purchased {tier} via Stripe") names = ", ".join(r.name for r in roles_to_add) print(f"[Bot] Assigned roles '{names}' to {discord_username}") else: print(f"[Bot] No valid roles found to assign") except discord.NotFound: print(f"[Bot] Member {discord_user_id} not found in guild (may have left)") except Exception as exc: print(f"[Bot] Role assignment error for {discord_user_id}: {exc}") return web.Response(status=200, text="OK") async def _start_webhook_server(): app = web.Application() app.router.add_post("/stripe/webhook", _handle_stripe_webhook) runner = web.AppRunner(app) await runner.setup() site = web.TCPSite(runner, "0.0.0.0", WEBHOOK_PORT) await site.start() print(f"[Webhook] Stripe webhook server listening on :{WEBHOOK_PORT}/stripe/webhook") # ───────────────────────────────────────────────────────────────────────────── # Entry point # ───────────────────────────────────────────────────────────────────────────── async def main(): async with bot: await _start_webhook_server() await bot.start(DISCORD_TOKEN) if __name__ == "__main__": asyncio.run(main())