import discord import asyncio import requests import json import io import qrcode import websockets import traceback from discord import File, Embed from discord.ext import commands # --- Configuration Loading --- try: with open("config.json", "r") as f: config = json.load(f) except FileNotFoundError: print("❌ Error: config.json not found. Please create it and fill in the details.") exit() except json.JSONDecodeError: print("❌ Error: config.json is not valid JSON. Please check its syntax.") exit() # Load values TOKEN = config.get("discord_token") GUILD_ID = int(config.get("guild_id", 0)) ROLE_ID = int(config.get("role_id", 0)) LNBITS_URL = config.get("lnbits_url") LNBITS_API_KEY = config.get("lnbits_api_key") PRICE = config.get("price") CHANNEL_ID = int(config.get("channelid", 0)) INVOICE_MESSAGE_TEMPLATE = config.get("invoicemessage", "Invoice for your purchase.") COMMAND_NAME = config.get("command_name", "support") # Validate if not LNBITS_URL or not LNBITS_API_KEY: print("❌ Error: LNBITS_URL or LNBITS_API_KEY is missing in config.json.") exit() clean_lnbits_http_url = LNBITS_URL.rstrip('/') if clean_lnbits_http_url.startswith("https://"): base_ws_url = clean_lnbits_http_url.replace("https://", "wss://", 1) elif clean_lnbits_http_url.startswith("http://"): base_ws_url = clean_lnbits_http_url.replace("http://", "ws://", 1) else: print(f"❌ Error: Invalid LNBITS_URL scheme: {LNBITS_URL}.") exit() LNBITS_WEBSOCKET_URL = f"{base_ws_url}/api/v1/ws/{LNBITS_API_KEY}" if not all([TOKEN, GUILD_ID, ROLE_ID, PRICE, CHANNEL_ID, COMMAND_NAME]): print("❌ Error: One or more essential configuration options are missing.") exit() # Discord client intents = discord.Intents.default() intents.members = True bot = commands.Bot(command_prefix="!", intents=intents) pending_invoices = {} async def assign_role_after_payment(payment_hash_received, payment_details_from_ws): print(f"DEBUG: ENTER assign_role_after_payment for hash: {payment_hash_received}") if payment_hash_received not in pending_invoices: print(f"ℹ️ Hash {payment_hash_received} not found in pending_invoices.") return user_id, original_interaction = pending_invoices.pop(payment_hash_received) print(f"DEBUG: Found pending invoice for user_id={user_id}, interaction_id={getattr(original_interaction, 'id', None)}") guild = bot.get_guild(GUILD_ID) if not guild: print(f"❌ Guild {GUILD_ID} not found.") return member = guild.get_member(user_id) if not member: try: member = await asyncio.wait_for(guild.fetch_member(user_id), timeout=10) except Exception as e: print(f"❌ Error fetching member: {e}") return role = guild.get_role(ROLE_ID) if not role: print(f"❌ Role ID {ROLE_ID} not found in guild.") return invoice_channel = bot.get_channel(CHANNEL_ID) if not invoice_channel: print(f"❌ Invoice channel ID {CHANNEL_ID} not found.") if role not in member.roles: try: await asyncio.wait_for(member.add_roles(role, reason="Paid Lightning Invoice"), timeout=10) if invoice_channel: await invoice_channel.send( f"🎉 {member.mention} has paid {PRICE} sats and been granted the '{role.name}' role!" ) except Exception as e: print(f"❌ Error assigning role: {e}") else: if invoice_channel: await invoice_channel.send( f"✅ {member.mention}, payment confirmed! You already have the '{role.name}' role." ) async def lnbits_websocket_listener(): await bot.wait_until_ready() print(f"👂 Connecting to LNBits WS at {LNBITS_WEBSOCKET_URL}") while True: try: async with websockets.connect(LNBITS_WEBSOCKET_URL) as ws: print("✅ WS connected.") while True: try: msg = await ws.recv() print(f"📬 WS message: {msg}") data = json.loads(msg) if isinstance(data.get("payment"), dict): p = data["payment"] h = p.get("checking_id") or p.get("payment_hash") amt = p.get("amount", 0) status = p.get("status") is_paid = (status == "success") or (p.get("paid") is True) or (p.get("pending") is False) if h and is_paid and amt > 0: print(f"✅ Confirmed payment hash={h}, amount={amt}, status={status}") bot.loop.create_task( assign_role_after_payment(h, p) ) else: print(f"ℹ️ Ignored update for hash={h}, status={status}, amount={amt}") else: print(f"⚠️ Unexpected WS format: {msg}") except websockets.exceptions.ConnectionClosed: print("⚠️ WS closed, reconnecting...") break except Exception as e: print(f"❌ Error handling WS message: {e}") except Exception as e: print(f"❌ WS connection error: {e}. Retrying in 15s...") await asyncio.sleep(15) @bot.event async def on_ready(): print(f"✅ Bot ready as {bot.user} ({bot.user.id})") bot.loop.create_task(lnbits_websocket_listener()) # Dynamic slash command name from config @bot.tree.command(name=COMMAND_NAME, description="Pay to get your role via Lightning.") async def dynamic_command(interaction: discord.Interaction): invoice_channel = bot.get_channel(CHANNEL_ID) if not invoice_channel: await interaction.response.send_message("❌ Invoice channel misconfigured.", ephemeral=True) return invoice_data = {"out": False, "amount": PRICE, "memo": f"Role for {interaction.user.display_name}"} headers = {"X-Api-Key": LNBITS_API_KEY, "Content-Type": "application/json"} loop = asyncio.get_running_loop() try: resp = await loop.run_in_executor(None, lambda: requests.post( f"{clean_lnbits_http_url}/api/v1/payments", json=invoice_data, headers=headers )) resp.raise_for_status() except Exception as e: await interaction.response.send_message("❌ Could not create invoice. Try again later.", ephemeral=True) print(f"❌ Invoice creation error: {e}") return if resp.status_code != 201: await interaction.response.send_message(f"❌ LNBits error ({resp.status_code}).", ephemeral=True) print(f"❌ LNBits error response: {resp.text}") return inv = resp.json() pr = inv.get("bolt11") h = inv.get("payment_hash") if not pr or not h: await interaction.response.send_message("❌ Invalid invoice data.", ephemeral=True) return pending_invoices[h] = (interaction.user.id, interaction) print(f"DEBUG: Stored pending invoice {h} for user {interaction.user.id}") buf = io.BytesIO() try: await loop.run_in_executor(None, lambda: qrcode.make(pr.upper()).save(buf, format="PNG")) buf.seek(0) qr_file = File(buf, filename="invoice_qr.png") except Exception as e: print(f"⚠️ QR generation failed: {e}") qr_file = None embed = Embed(title="⚡ Payment Required ⚡", description=INVOICE_MESSAGE_TEMPLATE) embed.add_field(name="Invoice", value=f"```{pr}```", inline=False) embed.add_field(name="Amount", value=f"{PRICE} sats", inline=True) embed.set_footer(text=h) if qr_file: embed.set_image(url="attachment://invoice_qr.png") try: await invoice_channel.send(content=interaction.user.mention, embed=embed, file=qr_file if qr_file else None) await interaction.response.send_message("✅ Invoice posted!", ephemeral=True) except Exception as e: print(f"❌ Error sending invoice message: {e}") await interaction.response.send_message("❌ Failed to post invoice.", ephemeral=True) if __name__ == "__main__": bot.run(TOKEN)