discord-lnbits-bot/discord_lnbits_bot.py

211 lines
8.4 KiB
Python
Raw Normal View History

2025-03-09 03:36:29 +00:00
import discord
import asyncio
import requests
import json
import io
import qrcode
import websockets
import traceback
2025-03-09 03:36:29 +00:00
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
2025-03-09 03:36:29 +00:00
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)}")
2025-03-09 03:36:29 +00:00
guild = bot.get_guild(GUILD_ID)
if not guild:
print(f"❌ Guild {GUILD_ID} not found.")
return
2025-03-09 03:36:29 +00:00
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
2025-03-09 03:36:29 +00:00
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}")
2025-03-09 03:36:29 +00:00
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
2025-03-09 03:36:29 +00:00
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}")
2025-03-09 03:36:29 +00:00
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}")
2025-03-09 03:36:29 +00:00
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)