diff --git a/lnbits/extensions/discordbot/Pipfile b/lnbits/extensions/discordbot/Pipfile new file mode 100644 index 00000000..d5820662 --- /dev/null +++ b/lnbits/extensions/discordbot/Pipfile @@ -0,0 +1,11 @@ +[[source]] +url = "https://pypi.python.org/simple" +verify_ssl = true +name = "pypi" + +[packages] + +[dev-packages] + +[requires] +python_version = "3.9" diff --git a/lnbits/extensions/discordbot/README.md b/lnbits/extensions/discordbot/README.md new file mode 100644 index 00000000..a1408317 --- /dev/null +++ b/lnbits/extensions/discordbot/README.md @@ -0,0 +1,34 @@ +# Discord Bot + +## Provide LNbits wallets for all your Discord users + +_This extension is a modifed version of LNbits [User Manager](../usermanager/README.md)_ + +The intended usage of this extension is to connect it to a specifically designed [Discord Bot](https://github.com/chrislennon/lnbits-discord-bot) leveraging LNbits as a community based lightning node. + +## Setup +This bot can target [lnbits.com](https://lnbits.com) or a self hosted instance. + +To setup and run the bot instructions are located [here](https://github.com/chrislennon/lnbits-discord-bot#installation) + +## Usage +This bot will allow users to interact with it in the following ways [full command list](https://github.com/chrislennon/lnbits-discord-bot#commands): + +`/create` Will create a wallet for the Discord user + - (currently limiting 1 Discord user == 1 LNbits user == 1 user wallet) + +![create](https://imgur.com/CWdDusE.png) + +`/balance` Will show the balance of the users wallet. + +![balance](https://imgur.com/tKeReCp.png) + +`/tip @user [amount]` Will sent money from one user to another + - If the recieving user does not have a wallet, one will be created for them + - The receiving user will receive a direct message from the bot with a link to their wallet + +![tip](https://imgur.com/K3tnChK.png) + +`/payme [amount] [description]` Will open an invoice that can be paid by any user + +![payme](https://imgur.com/dFvAqL3.png) diff --git a/lnbits/extensions/discordbot/__init__.py b/lnbits/extensions/discordbot/__init__.py new file mode 100644 index 00000000..ff60dd62 --- /dev/null +++ b/lnbits/extensions/discordbot/__init__.py @@ -0,0 +1,25 @@ +from fastapi import APIRouter +from fastapi.staticfiles import StaticFiles + +from lnbits.db import Database +from lnbits.helpers import template_renderer + +db = Database("ext_discordbot") + +discordbot_static_files = [ + { + "path": "/discordbot/static", + "app": StaticFiles(directory="lnbits/extensions/discordbot/static"), + "name": "discordbot_static", + } +] + +discordbot_ext: APIRouter = APIRouter(prefix="/discordbot", tags=["discordbot"]) + + +def discordbot_renderer(): + return template_renderer(["lnbits/extensions/discordbot/templates"]) + + +from .views import * # noqa +from .views_api import * # noqa diff --git a/lnbits/extensions/discordbot/config.json b/lnbits/extensions/discordbot/config.json new file mode 100644 index 00000000..eb674122 --- /dev/null +++ b/lnbits/extensions/discordbot/config.json @@ -0,0 +1,6 @@ +{ + "name": "Discord Bot", + "short_description": "Generate users and wallets", + "icon": "person_add", + "contributors": ["bitcoingamer21"] +} diff --git a/lnbits/extensions/discordbot/crud.py b/lnbits/extensions/discordbot/crud.py new file mode 100644 index 00000000..5661fcb4 --- /dev/null +++ b/lnbits/extensions/discordbot/crud.py @@ -0,0 +1,123 @@ +from typing import List, Optional + +from lnbits.core.crud import ( + create_account, + create_wallet, + delete_wallet, + get_payments, + get_user, +) +from lnbits.core.models import Payment + +from . import db +from .models import CreateUserData, Users, Wallets + +### Users + + +async def create_discordbot_user(data: CreateUserData) -> Users: + account = await create_account() + user = await get_user(account.id) + assert user, "Newly created user couldn't be retrieved" + + wallet = await create_wallet(user_id=user.id, wallet_name=data.wallet_name) + + await db.execute( + """ + INSERT INTO discordbot.users (id, name, admin, discord_id) + VALUES (?, ?, ?, ?) + """, + (user.id, data.user_name, data.admin_id, data.discord_id), + ) + + await db.execute( + """ + INSERT INTO discordbot.wallets (id, admin, name, "user", adminkey, inkey) + VALUES (?, ?, ?, ?, ?, ?) + """, + ( + wallet.id, + data.admin_id, + data.wallet_name, + user.id, + wallet.adminkey, + wallet.inkey, + ), + ) + + user_created = await get_discordbot_user(user.id) + assert user_created, "Newly created user couldn't be retrieved" + return user_created + + +async def get_discordbot_user(user_id: str) -> Optional[Users]: + row = await db.fetchone("SELECT * FROM discordbot.users WHERE id = ?", (user_id,)) + return Users(**row) if row else None + + +async def get_discordbot_users(user_id: str) -> List[Users]: + rows = await db.fetchall( + "SELECT * FROM discordbot.users WHERE admin = ?", (user_id,) + ) + + return [Users(**row) for row in rows] + + +async def delete_discordbot_user(user_id: str) -> None: + wallets = await get_discordbot_wallets(user_id) + for wallet in wallets: + await delete_wallet(user_id=user_id, wallet_id=wallet.id) + + await db.execute("DELETE FROM discordbot.users WHERE id = ?", (user_id,)) + await db.execute("""DELETE FROM discordbot.wallets WHERE "user" = ?""", (user_id,)) + + +### Wallets + + +async def create_discordbot_wallet( + user_id: str, wallet_name: str, admin_id: str +) -> Wallets: + wallet = await create_wallet(user_id=user_id, wallet_name=wallet_name) + await db.execute( + """ + INSERT INTO discordbot.wallets (id, admin, name, "user", adminkey, inkey) + VALUES (?, ?, ?, ?, ?, ?) + """, + (wallet.id, admin_id, wallet_name, user_id, wallet.adminkey, wallet.inkey), + ) + wallet_created = await get_discordbot_wallet(wallet.id) + assert wallet_created, "Newly created wallet couldn't be retrieved" + return wallet_created + + +async def get_discordbot_wallet(wallet_id: str) -> Optional[Wallets]: + row = await db.fetchone( + "SELECT * FROM discordbot.wallets WHERE id = ?", (wallet_id,) + ) + return Wallets(**row) if row else None + + +async def get_discordbot_wallets(admin_id: str) -> Optional[Wallets]: + rows = await db.fetchall( + "SELECT * FROM discordbot.wallets WHERE admin = ?", (admin_id,) + ) + return [Wallets(**row) for row in rows] + + +async def get_discordbot_users_wallets(user_id: str) -> Optional[Wallets]: + rows = await db.fetchall( + """SELECT * FROM discordbot.wallets WHERE "user" = ?""", (user_id,) + ) + return [Wallets(**row) for row in rows] + + +async def get_discordbot_wallet_transactions(wallet_id: str) -> Optional[Payment]: + return await get_payments( + wallet_id=wallet_id, complete=True, pending=False, outgoing=True, incoming=True + ) + + +async def delete_discordbot_wallet(wallet_id: str, user_id: str) -> None: + await delete_wallet(user_id=user_id, wallet_id=wallet_id) + await db.execute("DELETE FROM discordbot.wallets WHERE id = ?", (wallet_id,)) diff --git a/lnbits/extensions/discordbot/migrations.py b/lnbits/extensions/discordbot/migrations.py new file mode 100644 index 00000000..ababfd7a --- /dev/null +++ b/lnbits/extensions/discordbot/migrations.py @@ -0,0 +1,30 @@ +async def m001_initial(db): + """ + Initial users table. + """ + await db.execute( + """ + CREATE TABLE discordbot.users ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + admin TEXT NOT NULL, + discord_id TEXT + ); + """ + ) + + """ + Initial wallets table. + """ + await db.execute( + """ + CREATE TABLE discordbot.wallets ( + id TEXT PRIMARY KEY, + admin TEXT NOT NULL, + name TEXT NOT NULL, + "user" TEXT NOT NULL, + adminkey TEXT NOT NULL, + inkey TEXT NOT NULL + ); + """ + ) diff --git a/lnbits/extensions/discordbot/models.py b/lnbits/extensions/discordbot/models.py new file mode 100644 index 00000000..4be367f8 --- /dev/null +++ b/lnbits/extensions/discordbot/models.py @@ -0,0 +1,36 @@ +from sqlite3 import Row + +from fastapi.param_functions import Query +from pydantic import BaseModel +from typing import Optional + + +class CreateUserData(BaseModel): + user_name: str = Query(...) + wallet_name: str = Query(...) + admin_id: str = Query(...) + discord_id: str = Query("") + +class CreateUserWallet(BaseModel): + user_id: str = Query(...) + wallet_name: str = Query(...) + admin_id: str = Query(...) + + +class Users(BaseModel): + id: str + name: str + admin: str + discord_id: str + +class Wallets(BaseModel): + id: str + admin: str + name: str + user: str + adminkey: str + inkey: str + + @classmethod + def from_row(cls, row: Row) -> "Wallets": + return cls(**dict(row)) diff --git a/lnbits/extensions/discordbot/static/stack.png b/lnbits/extensions/discordbot/static/stack.png new file mode 100644 index 00000000..3b987db1 Binary files /dev/null and b/lnbits/extensions/discordbot/static/stack.png differ diff --git a/lnbits/extensions/discordbot/templates/discordbot/_api_docs.html b/lnbits/extensions/discordbot/templates/discordbot/_api_docs.html new file mode 100644 index 00000000..40fcfb12 --- /dev/null +++ b/lnbits/extensions/discordbot/templates/discordbot/_api_docs.html @@ -0,0 +1,260 @@ + + + +
+ Discord Bot: Connect Discord users to LNbits. +
+

+ Connect your LNbits instance to a Discord Bot leveraging LNbits as a community based lightning node.
+ + Created by, Chris Lennon
+ + Based on User Manager, by Ben Arc +

+
+
+
+ + + + + GET + /discordbot/api/v1/users +
Body (application/json)
+
+ Returns 201 CREATED (application/json) +
+ JSON list of users +
Curl example
+ curl -X GET {{ request.base_url }}discordbot/api/v1/users -H + "X-Api-Key: {{ user.wallets[0].inkey }}" + +
+
+
+ + + + GET + /discordbot/api/v1/users/<user_id> +
Body (application/json)
+
+ Returns 201 CREATED (application/json) +
+ JSON list of users +
Curl example
+ curl -X GET {{ request.base_url + }}discordbot/api/v1/users/<user_id> -H "X-Api-Key: {{ + user.wallets[0].inkey }}" + +
+
+
+ + + + GET + /discordbot/api/v1/wallets/<user_id> +
Headers
+ {"X-Api-Key": <string>} +
Body (application/json)
+
+ Returns 201 CREATED (application/json) +
+ JSON wallet data +
Curl example
+ curl -X GET {{ request.base_url + }}discordbot/api/v1/wallets/<user_id> -H "X-Api-Key: {{ + user.wallets[0].inkey }}" + +
+
+
+ + + + GET + /discordbot/api/v1/wallets<wallet_id> +
Headers
+ {"X-Api-Key": <string>} +
Body (application/json)
+
+ Returns 201 CREATED (application/json) +
+ JSON a wallets transactions +
Curl example
+ curl -X GET {{ request.base_url + }}discordbot/api/v1/wallets<wallet_id> -H "X-Api-Key: {{ + user.wallets[0].inkey }}" + +
+
+
+ + + + POST + /discordbot/api/v1/users +
Headers
+ {"X-Api-Key": <string>, "Content-type": + "application/json"} +
+ Body (application/json) - "admin_id" is a YOUR user ID +
+ {"admin_id": <string>, "user_name": <string>, + "wallet_name": <string>,"discord_id": <string>} +
+ Returns 201 CREATED (application/json) +
+ {"id": <string>, "name": <string>, "admin": + <string>, "discord_id": <string>} +
Curl example
+ curl -X POST {{ request.base_url }}discordbot/api/v1/users -d + '{"admin_id": "{{ user.id }}", "wallet_name": <string>, + "user_name": <string>, "discord_id": <string>}' -H "X-Api-Key: {{ + user.wallets[0].inkey }}" -H "Content-type: application/json" + +
+
+
+ + + + POST + /discordbot/api/v1/wallets +
Headers
+ {"X-Api-Key": <string>, "Content-type": + "application/json"} +
+ Body (application/json) - "admin_id" is a YOUR user ID +
+ {"user_id": <string>, "wallet_name": <string>, + "admin_id": <string>} +
+ Returns 201 CREATED (application/json) +
+ {"id": <string>, "admin": <string>, "name": + <string>, "user": <string>, "adminkey": <string>, + "inkey": <string>} +
Curl example
+ curl -X POST {{ request.base_url }}discordbot/api/v1/wallets -d + '{"user_id": <string>, "wallet_name": <string>, + "admin_id": "{{ user.id }}"}' -H "X-Api-Key: {{ user.wallets[0].inkey + }}" -H "Content-type: application/json" + +
+
+
+ + + + DELETE + /discordbot/api/v1/users/<user_id> +
Headers
+ {"X-Api-Key": <string>} +
Curl example
+ curl -X DELETE {{ request.base_url + }}discordbot/api/v1/users/<user_id> -H "X-Api-Key: {{ + user.wallets[0].inkey }}" + +
+
+
+ + + + DELETE + /discordbot/api/v1/wallets/<wallet_id> +
Headers
+ {"X-Api-Key": <string>} +
Curl example
+ curl -X DELETE {{ request.base_url + }}discordbot/api/v1/wallets/<wallet_id> -H "X-Api-Key: {{ + user.wallets[0].inkey }}" + +
+
+
+ + + + POST + /discordbot/api/v1/extensions +
Headers
+ {"X-Api-Key": <string>} +
Curl example
+ curl -X POST {{ request.base_url }}discordbot/api/v1/extensions -d + '{"userid": <string>, "extension": <string>, "active": + <integer>}' -H "X-Api-Key: {{ user.wallets[0].inkey }}" -H + "Content-type: application/json" + +
+
+
+
diff --git a/lnbits/extensions/discordbot/templates/discordbot/index.html b/lnbits/extensions/discordbot/templates/discordbot/index.html new file mode 100644 index 00000000..782f8bb6 --- /dev/null +++ b/lnbits/extensions/discordbot/templates/discordbot/index.html @@ -0,0 +1,464 @@ +{% extends "base.html" %} {% from "macros.jinja" import window_vars with context +%} {% block page %} +
+
+ + +
+ This extension is designed to be used through its API by a Discord Bot, + currently you have to install the bot + yourself
+ + Soon™ there will be a much easier one-click install discord bot... +
+
+ + + +
+
+
Users
+
+
+ Export to CSV +
+
+ + {% raw %} + + + {% endraw %} + +
+
+ + + +
+
+
Wallets
+
+
+ Export to CSV +
+
+ + {% raw %} + + + {% endraw %} + +
+
+
+ +
+ + +
LNbits Discord Bot Extension + +
+
+ + + {% include "discordbot/_api_docs.html" %} + +
+
+ + + + + + + + + Create User + Cancel + + + + + + + + + + + Create Wallet + Cancel + + + +
+{% endblock %} {% block scripts %} {{ window_vars(user) }} + +{% endblock %} diff --git a/lnbits/extensions/discordbot/views.py b/lnbits/extensions/discordbot/views.py new file mode 100644 index 00000000..a5395e21 --- /dev/null +++ b/lnbits/extensions/discordbot/views.py @@ -0,0 +1,15 @@ +from fastapi import Request +from fastapi.params import Depends +from starlette.responses import HTMLResponse + +from lnbits.core.models import User +from lnbits.decorators import check_user_exists + +from . import discordbot_ext, discordbot_renderer + + +@discordbot_ext.get("/", response_class=HTMLResponse) +async def index(request: Request, user: User = Depends(check_user_exists)): + return discordbot_renderer().TemplateResponse( + "discordbot/index.html", {"request": request, "user": user.dict()} + ) diff --git a/lnbits/extensions/discordbot/views_api.py b/lnbits/extensions/discordbot/views_api.py new file mode 100644 index 00000000..64d1df1a --- /dev/null +++ b/lnbits/extensions/discordbot/views_api.py @@ -0,0 +1,127 @@ +from http import HTTPStatus + +from fastapi import Query +from fastapi.params import Depends +from starlette.exceptions import HTTPException + +from lnbits.core import update_user_extension +from lnbits.core.crud import get_user +from lnbits.decorators import WalletTypeInfo, get_key_type + +from . import discordbot_ext +from .crud import ( + create_discordbot_user, + create_discordbot_wallet, + delete_discordbot_user, + delete_discordbot_wallet, + get_discordbot_user, + get_discordbot_users, + get_discordbot_users_wallets, + get_discordbot_wallet, + get_discordbot_wallet_transactions, + get_discordbot_wallets, +) +from .models import CreateUserData, CreateUserWallet + +# Users + + +@discordbot_ext.get("/api/v1/users", status_code=HTTPStatus.OK) +async def api_discordbot_users(wallet: WalletTypeInfo = Depends(get_key_type)): + user_id = wallet.wallet.user + return [user.dict() for user in await get_discordbot_users(user_id)] + + +@discordbot_ext.get("/api/v1/users/{user_id}", status_code=HTTPStatus.OK) +async def api_discordbot_user(user_id, wallet: WalletTypeInfo = Depends(get_key_type)): + user = await get_discordbot_user(user_id) + return user.dict() + + +@discordbot_ext.post("/api/v1/users", status_code=HTTPStatus.CREATED) +async def api_discordbot_users_create( + data: CreateUserData, wallet: WalletTypeInfo = Depends(get_key_type) +): + user = await create_discordbot_user(data) + full = user.dict() + full["wallets"] = [ + wallet.dict() for wallet in await get_discordbot_users_wallets(user.id) + ] + return full + + +@discordbot_ext.delete("/api/v1/users/{user_id}") +async def api_discordbot_users_delete( + user_id, wallet: WalletTypeInfo = Depends(get_key_type) +): + user = await get_discordbot_user(user_id) + if not user: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="User does not exist." + ) + await delete_discordbot_user(user_id) + raise HTTPException(status_code=HTTPStatus.NO_CONTENT) + + +# Activate Extension + + +@discordbot_ext.post("/api/v1/extensions") +async def api_discordbot_activate_extension( + extension: str = Query(...), userid: str = Query(...), active: bool = Query(...) +): + user = await get_user(userid) + if not user: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="User does not exist." + ) + update_user_extension(user_id=userid, extension=extension, active=active) + return {"extension": "updated"} + + +# Wallets + + +@discordbot_ext.post("/api/v1/wallets") +async def api_discordbot_wallets_create( + data: CreateUserWallet, wallet: WalletTypeInfo = Depends(get_key_type) +): + user = await create_discordbot_wallet( + user_id=data.user_id, wallet_name=data.wallet_name, admin_id=data.admin_id + ) + return user.dict() + + +@discordbot_ext.get("/api/v1/wallets") +async def api_discordbot_wallets(wallet: WalletTypeInfo = Depends(get_key_type)): + admin_id = wallet.wallet.user + return [wallet.dict() for wallet in await get_discordbot_wallets(admin_id)] + + +@discordbot_ext.get("/api/v1/transactions/{wallet_id}") +async def api_discordbot_wallet_transactions( + wallet_id, wallet: WalletTypeInfo = Depends(get_key_type) +): + return await get_discordbot_wallet_transactions(wallet_id) + + +@discordbot_ext.get("/api/v1/wallets/{user_id}") +async def api_discordbot_users_wallets( + user_id, wallet: WalletTypeInfo = Depends(get_key_type) +): + return [ + s_wallet.dict() for s_wallet in await get_discordbot_users_wallets(user_id) + ] + + +@discordbot_ext.delete("/api/v1/wallets/{wallet_id}") +async def api_discordbot_wallets_delete( + wallet_id, wallet: WalletTypeInfo = Depends(get_key_type) +): + get_wallet = await get_discordbot_wallet(wallet_id) + if not get_wallet: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="Wallet does not exist." + ) + await delete_discordbot_wallet(wallet_id, get_wallet.user) + raise HTTPException(status_code=HTTPStatus.NO_CONTENT)