From 4bbe3029d996a32a25dd83ba2437fd1de22a6a41 Mon Sep 17 00:00:00 2001 From: saulteafarmer Date: Sun, 9 Mar 2025 03:36:29 +0000 Subject: [PATCH] Initial commit --- README.md | 180 +++++++++++++++++++++++++++++++++++++++++- config.json | 11 +++ discord_lnbits_bot.py | 137 ++++++++++++++++++++++++++++++++ requirements.txt | 4 + 4 files changed, 331 insertions(+), 1 deletion(-) create mode 100644 config.json create mode 100644 discord_lnbits_bot.py create mode 100644 requirements.txt diff --git a/README.md b/README.md index 235ee8e..4f3f25f 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,180 @@ -# discord-lnbits-bot +# Discord LNbits Bot +This bot allows users to purchase a **Discord role** using **Bitcoin Lightning payments** through LNbits. Users request an invoice via a **slash command**, and the bot automatically assigns the role **once payment is confirmed**. + +## Features + +✅ Users request an invoice using `/support` (configurable). +✅ The bot generates a **Lightning invoice** via LNbits. +✅ Once paid, the bot assigns a **Discord role**. +✅ Configurable check intervals & max attempts to verify payments. + +--- + +## Requirements + +- **Ubuntu 24.04** +- **Python 3.10+** +- **A Discord Bot Token** (from the [Developer Portal](https://discord.com/developers/applications)) +- **LNbits Wallet Invoice Key** +- **A running LNbits instance** (e.g., [Sats.Love](https://sats.love/)) + +--- + +## 1️⃣ Setting Up Your Bot + +### Generate a Proper Bot Invite Link + +1. **Go to**: [Discord Developer Portal](https://discord.com/developers/applications) +2. **Click New Application** +3. Go to **OAuth2 > URL Generator** +4. **Check these Scopes**: + - ✅ **bot** + - ✅ **applications.commands** (for slash commands) +4. **Under Bot Permissions**, check: + - ✅ **Manage Roles** + - ✅ **Send Messages** + - ✅ **Embed Links** + - ✅ **Read Message History** +5. **Copy & paste the generated link into your browser.** +6. **Select your Discord server** and click **Authorize**. + +--- + +### Enable Privileged Intents + +1. **Go to** [Discord Developer Portal](https://discord.com/developers/applications). +2. **Select your bot application.** +3. **Navigate to** `Bot` in the left sidebar. +4. **Scroll down to** `Privileged Gateway Intents`. +5. **Enable** the following: + - ✅ **Presence Intent** *(optional)* + - ✅ **Server Members Intent** *(⚠️ Required for managing roles!)* + - ✅ **Message Content Intent** *(optional, only if reading messages is needed)* +6. **Click Save Changes.** + +--- + +### Ensure Correct Role Permissions + +1. **Go to Server Settings** → `Roles`. +2. **Drag the bot’s role ABOVE** the role it needs to assign. +3. **Ensure the bot has these permissions**: + - ✅ **Manage Roles** + - ✅ **Read Messages** + - ✅ **Send Messages** + +--- + +## 2️⃣ Installation Guide + +### 1. Install System Dependencies + +```bash +sudo apt update && sudo apt upgrade -y +sudo apt install python3 python3-pip python3-venv -y +``` + +### 2. Clone the Repository + +```bash +git clone https://code.rustysats.com/saulteafarmer/discord-lnbits-bot +cd discord-lnbits-bot +``` + +### 3. Create & Activate Virtual Environment + +```bash +python3 -m venv venv +source venv/bin/activate +``` + +### 4. Install Dependencies + +```bash +pip install -r requirements.txt +``` + +--- + +## 3️⃣ Find Your Discord Guild ID & Role ID + +Before configuring the bot, retrieve your **Guild ID** and **Role ID**. + +### 🔹 How to Find Your Guild ID (Server ID): + +1. Open **Discord** → Go to **User Settings** (⚙️). +2. Scroll down to **Advanced** → Enable **Developer Mode**. +3. **Right-click your server name** (left sidebar) → Click **Copy ID**. +4. **Save this ID** for later (`guild_id`). + +### 🔹 How to Find Your Role ID: + +1. Open your **Discord Server**. +2. Go to **Server Settings** → **Roles**. +3. **Right-click on the role** you want to assign → Click **Copy ID**. +4. **Save this ID** for later (`role_id`). + +--- + +## 4️⃣ Configure the Bot + +Edit the `config.json` file inside the bot directory: + +```bash +sudo nano config.json +``` + +```json +{ + "discord_token": "YOUR_DISCORD_BOT_TOKEN", + "guild_id": "YOUR_GUILD_ID", + "role_id": "YOUR_ROLE_ID", + "lnbits_url": "https://sats.love", + "lnbits_api_key": "YOUR_INVOICE_READ_KEY", + "price": 1000, + "command_name": "support", + "check_interval": 30, + "max_checks": 20 +} +``` + +### 🔹 What Each Setting Does: + +- **`discord_token`** → Your bot token from Discord Developer Portal +- **`guild_id`** → Your Discord server ID +- **`role_id`** → The Discord role the bot will assign +- **`lnbits_url`** → Base URL of your LNbits instance +- **`lnbits_api_key`** → **Invoice-only key** from LNbits (⚠️ NOT an admin key) +- **`price`** → Price in **satoshis** (e.g., `1000` = 1000 sats) +- **`command_name`** → Name of the command (default: `support`) +- **`check_interval`** → How often (in seconds) the bot checks for payments +- **`max_checks`** → How many times the bot will check before stopping + +--- + +## 5️⃣ Run the Bot + +```bash +python3 discord_lnbits_bot.py +``` + +--- + +## 6️⃣ Running the Bot in the Background + +```bash +nohup python3 discord_lnbits_bot.py & +``` + +🔹 **To stop the bot**: + +```bash +pkill -f discord_lnbits_bot.py +``` + +--- + +## License + +This project is **open-source** and free to use. Contributions welcome! \ No newline at end of file diff --git a/config.json b/config.json new file mode 100644 index 0000000..c2745eb --- /dev/null +++ b/config.json @@ -0,0 +1,11 @@ +{ + "discord_token": "YOUR_DISCORD_BOT_TOKEN", + "guild_id": "YOUR_GUILD_ID", + "role_id": "YOUR_ROLE_ID", + "lnbits_url": "https://sats.love", + "lnbits_api_key": "YOUR_INVOICE_READ_KEY", + "price": 1000, + "command_name": "support", + "check_interval": 30, + "max_checks": 20 + } \ No newline at end of file diff --git a/discord_lnbits_bot.py b/discord_lnbits_bot.py new file mode 100644 index 0000000..d8517df --- /dev/null +++ b/discord_lnbits_bot.py @@ -0,0 +1,137 @@ +import discord +import asyncio +import requests +import json +import io +import qrcode +from discord import File, Embed +from discord.ext import commands, tasks + +with open("config.json", "r") as f: + config = json.load(f) + +TOKEN = config["discord_token"] +GUILD_ID = int(config["guild_id"]) +ROLE_ID = int(config["role_id"]) +LNBITS_URL = config["lnbits_url"] +LNBITS_API_KEY = config["lnbits_api_key"] +PRICE = config["price"] +CHECK_INTERVAL = config["check_interval"] +MAX_CHECKS = config["max_checks"] + +intents = discord.Intents.default() +intents.members = True +intents.message_content = True + +bot = commands.Bot(command_prefix="!", intents=intents) + +pending_invoices = {} + +@bot.event +async def on_ready(): + """Called when the bot successfully logs in.""" + print(f"✅ Logged in as {bot.user}") + print("Bot is in the following guilds:") + for g in bot.guilds: + print(f" - {g.name} (ID: {g.id})") + + synced_cmds = await bot.tree.sync() + print("✅ Synced global commands:", synced_cmds) + + print(f"🔄 Checking invoices every {CHECK_INTERVAL} seconds...") + check_invoices.start() + +@bot.tree.command(name="support", description="Pay a Lightning invoice to get your role!") +async def support_command(interaction: discord.Interaction): + user_id = interaction.user.id + + invoice_data = { + "out": False, + "amount": PRICE, + "memo": "Lightning Payment" + } + headers = { + "X-Api-Key": LNBITS_API_KEY, + "Content-Type": "application/json" + } + + resp = requests.post(f"{LNBITS_URL}/api/v1/payments", json=invoice_data, headers=headers) + if resp.status_code == 201: + invoice_json = resp.json() + payment_request = invoice_json["payment_request"] + payment_hash = invoice_json["payment_hash"] + + pending_invoices[payment_hash] = (user_id, 0) + + qr_buffer = io.BytesIO() + qrcode.make(payment_request).save(qr_buffer, format="PNG") + qr_buffer.seek(0) + qr_file = File(fp=qr_buffer, filename="invoice_qr.png") + + embed = Embed( + title="Payment Invoice", + description="Please pay the following Lightning invoice to complete your purchase." + ) + embed.add_field(name="Invoice", value=f"```{payment_request}```", inline=False) + embed.add_field(name="Amount", value=f"{PRICE} sats", inline=True) + embed.add_field(name="Requesting User", value=f"{interaction.user.display_name}", inline=True) + embed.set_image(url="attachment://invoice_qr.png") + + await interaction.user.send( + content=f"{interaction.user.display_name}, please pay **{PRICE} sats** using the Lightning Network.", + embed=embed, + file=qr_file + ) + + await interaction.response.send_message("✅ I've sent you a payment invoice via DM!", ephemeral=True) + + else: + await interaction.response.send_message("❌ Failed to generate invoice. Try again later.", ephemeral=True) + print(f"❌ LNbits Error: {resp.text}") + +@tasks.loop(seconds=CHECK_INTERVAL) +async def check_invoices(): + if not pending_invoices: + return + + guild = discord.utils.get(bot.guilds, id=GUILD_ID) + if guild is None: + print(f"⚠️ Bot not in server with ID {GUILD_ID}") + return + + headers = { + "X-Api-Key": LNBITS_API_KEY, + "Content-Type": "application/json" + } + + invoices_to_remove = [] + + for payment_hash, (user_id, attempts) in pending_invoices.items(): + if attempts >= MAX_CHECKS: + invoices_to_remove.append(payment_hash) + continue + + status_resp = requests.get(f"{LNBITS_URL}/api/v1/payments/{payment_hash}", headers=headers) + + if status_resp.status_code == 200: + payment_status = status_resp.json() + if payment_status.get("paid", False): + member = guild.get_member(user_id) + if member: + role = guild.get_role(ROLE_ID) + if role: + await member.add_roles(role) + await member.send("✅ **Thank you! Your role has been assigned.**") + print(f"🎉 Role assigned to {member.name}") + invoices_to_remove.append(payment_hash) + else: + print(f"❌ Role ID {ROLE_ID} not found!") + else: + print(f"❌ Member {user_id} not found!") + + pending_invoices[payment_hash] = (user_id, attempts + 1) + + for done_hash in invoices_to_remove: + del pending_invoices[done_hash] + +bot.run(TOKEN) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..b17f6e2 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +discord.py +requests +qrcode[pil] +asyncio \ No newline at end of file