diff --git a/.gitignore b/.gitignore
index 79ff36abf..74da5d599 100644
--- a/.gitignore
+++ b/.gitignore
@@ -44,5 +44,5 @@ docker
fly.toml
# Ignore extensions (post installable extension PR)
-extensions/
-upgrades/
\ No newline at end of file
+extensions/*
+upgrades/
diff --git a/.prettierignore b/.prettierignore
index 776b0b093..2844476d4 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -5,4 +5,6 @@
*.yml
-**/lnbits/static
+**/lnbits/static/vendor
+**/lnbits/static/bundle.*
+**/lnbits/static/css/*
diff --git a/lnbits/extension_manager.py b/lnbits/extension_manager.py
index 7eff9480b..f9b5ce011 100644
--- a/lnbits/extension_manager.py
+++ b/lnbits/extension_manager.py
@@ -51,13 +51,16 @@ class Extension(NamedTuple):
)
+# All subdirectories in the current directory, not recursive.
+
+
class ExtensionManager:
def __init__(self, include_disabled_exts=False):
self._disabled: List[str] = settings.lnbits_disabled_extensions
self._admin_only: List[str] = settings.lnbits_admin_extensions
- self._extension_folders: List[str] = [
- x[1] for x in os.walk(os.path.join(settings.lnbits_path, "extensions"))
- ][0]
+ p = Path(settings.lnbits_path, "extensions")
+ os.makedirs(p, exist_ok=True)
+ self._extension_folders: List[Path] = [f for f in p.iterdir() if f.is_dir()]
@property
def extensions(self) -> List[Extension]:
@@ -70,11 +73,7 @@ class ExtensionManager:
ext for ext in self._extension_folders if ext not in self._disabled
]:
try:
- with open(
- os.path.join(
- settings.lnbits_path, "extensions", extension, "config.json"
- )
- ) as json_file:
+ with open(extension / "config.json") as json_file:
config = json.load(json_file)
is_valid = True
is_admin_only = True if extension in self._admin_only else False
@@ -83,9 +82,10 @@ class ExtensionManager:
is_valid = False
is_admin_only = False
+ *_, extension_code = extension.parts
output.append(
Extension(
- extension,
+ extension_code,
is_valid,
is_admin_only,
config.get("name"),
diff --git a/tests/extensions/__init__.py b/lnbits/extensions/.gitkeep
similarity index 100%
rename from tests/extensions/__init__.py
rename to lnbits/extensions/.gitkeep
diff --git a/lnbits/extensions/invoices/README.md b/lnbits/extensions/invoices/README.md
deleted file mode 100644
index cf3e8be02..000000000
--- a/lnbits/extensions/invoices/README.md
+++ /dev/null
@@ -1,19 +0,0 @@
-# Invoices
-
-## Create invoices that you can send to your client to pay online over Lightning.
-
-This extension allows users to create "traditional" invoices (not in the lightning sense) that contain one or more line items. Line items are denominated in a user-configurable fiat currency. Each invoice contains one or more payments up to the total of the invoice. Each invoice creates a public link that can be shared with a customer that they can use to (partially or in full) pay the invoice.
-
-## Usage
-
-1. Create an invoice by clicking "NEW INVOICE"\
- 
-2. Fill the options for your INVOICE
- - select the wallet
- - select the fiat currency the invoice will be denominated in
- - select a status for the invoice (default is draft)
- - enter a company name, first name, last name, email, phone & address (optional)
- - add one or more line items
- - enter a name & price for each line item
-3. You can then use share your invoice link with your customer to receive payment\
- 
diff --git a/lnbits/extensions/invoices/__init__.py b/lnbits/extensions/invoices/__init__.py
deleted file mode 100644
index 735e95d81..000000000
--- a/lnbits/extensions/invoices/__init__.py
+++ /dev/null
@@ -1,36 +0,0 @@
-import asyncio
-
-from fastapi import APIRouter
-from starlette.staticfiles import StaticFiles
-
-from lnbits.db import Database
-from lnbits.helpers import template_renderer
-from lnbits.tasks import catch_everything_and_restart
-
-db = Database("ext_invoices")
-
-invoices_static_files = [
- {
- "path": "/invoices/static",
- "app": StaticFiles(directory="lnbits/extensions/invoices/static"),
- "name": "invoices_static",
- }
-]
-
-invoices_ext: APIRouter = APIRouter(prefix="/invoices", tags=["invoices"])
-
-
-def invoices_renderer():
- return template_renderer(["lnbits/extensions/invoices/templates"])
-
-
-from .tasks import wait_for_paid_invoices
-
-
-def invoices_start():
- loop = asyncio.get_event_loop()
- loop.create_task(catch_everything_and_restart(wait_for_paid_invoices))
-
-
-from .views import * # noqa: F401,F403
-from .views_api import * # noqa: F401,F403
diff --git a/lnbits/extensions/invoices/config.json b/lnbits/extensions/invoices/config.json
deleted file mode 100644
index 1f0e4cded..000000000
--- a/lnbits/extensions/invoices/config.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "name": "Invoices",
- "short_description": "Create invoices for your clients.",
- "tile": "/invoices/static/image/invoices.png",
- "contributors": ["leesalminen"]
-}
diff --git a/lnbits/extensions/invoices/crud.py b/lnbits/extensions/invoices/crud.py
deleted file mode 100644
index 396528025..000000000
--- a/lnbits/extensions/invoices/crud.py
+++ /dev/null
@@ -1,210 +0,0 @@
-from typing import List, Optional, Union
-
-from lnbits.helpers import urlsafe_short_hash
-
-from . import db
-from .models import (
- CreateInvoiceData,
- CreateInvoiceItemData,
- Invoice,
- InvoiceItem,
- Payment,
- UpdateInvoiceData,
- UpdateInvoiceItemData,
-)
-
-
-async def get_invoice(invoice_id: str) -> Optional[Invoice]:
- row = await db.fetchone(
- "SELECT * FROM invoices.invoices WHERE id = ?", (invoice_id,)
- )
- return Invoice.from_row(row) if row else None
-
-
-async def get_invoice_items(invoice_id: str) -> List[InvoiceItem]:
- rows = await db.fetchall(
- "SELECT * FROM invoices.invoice_items WHERE invoice_id = ?", (invoice_id,)
- )
-
- return [InvoiceItem.from_row(row) for row in rows]
-
-
-async def get_invoice_item(item_id: str) -> Optional[InvoiceItem]:
- row = await db.fetchone(
- "SELECT * FROM invoices.invoice_items WHERE id = ?", (item_id,)
- )
- return InvoiceItem.from_row(row) if row else None
-
-
-async def get_invoice_total(items: List[InvoiceItem]) -> int:
- return sum(item.amount for item in items)
-
-
-async def get_invoices(wallet_ids: Union[str, List[str]]) -> List[Invoice]:
- if isinstance(wallet_ids, str):
- wallet_ids = [wallet_ids]
-
- q = ",".join(["?"] * len(wallet_ids))
- rows = await db.fetchall(
- f"SELECT * FROM invoices.invoices WHERE wallet IN ({q})", (*wallet_ids,)
- )
-
- return [Invoice.from_row(row) for row in rows]
-
-
-async def get_invoice_payments(invoice_id: str) -> List[Payment]:
- rows = await db.fetchall(
- "SELECT * FROM invoices.payments WHERE invoice_id = ?", (invoice_id,)
- )
-
- return [Payment.from_row(row) for row in rows]
-
-
-async def get_invoice_payment(payment_id: str) -> Optional[Payment]:
- row = await db.fetchone(
- "SELECT * FROM invoices.payments WHERE id = ?", (payment_id,)
- )
- return Payment.from_row(row) if row else None
-
-
-async def get_payments_total(payments: List[Payment]) -> int:
- return sum(item.amount for item in payments)
-
-
-async def create_invoice_internal(wallet_id: str, data: CreateInvoiceData) -> Invoice:
- invoice_id = urlsafe_short_hash()
- await db.execute(
- """
- INSERT INTO invoices.invoices (id, wallet, status, currency, company_name, first_name, last_name, email, phone, address)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
- """,
- (
- invoice_id,
- wallet_id,
- data.status,
- data.currency,
- data.company_name,
- data.first_name,
- data.last_name,
- data.email,
- data.phone,
- data.address,
- ),
- )
-
- invoice = await get_invoice(invoice_id)
- assert invoice, "Newly created invoice couldn't be retrieved"
- return invoice
-
-
-async def create_invoice_items(
- invoice_id: str, data: List[CreateInvoiceItemData]
-) -> List[InvoiceItem]:
- for item in data:
- item_id = urlsafe_short_hash()
- await db.execute(
- """
- INSERT INTO invoices.invoice_items (id, invoice_id, description, amount)
- VALUES (?, ?, ?, ?)
- """,
- (
- item_id,
- invoice_id,
- item.description,
- int(item.amount * 100),
- ),
- )
-
- invoice_items = await get_invoice_items(invoice_id)
- return invoice_items
-
-
-async def update_invoice_internal(
- wallet_id: str, data: Union[UpdateInvoiceData, Invoice]
-) -> Invoice:
- await db.execute(
- """
- UPDATE invoices.invoices
- SET wallet = ?, currency = ?, status = ?, company_name = ?, first_name = ?, last_name = ?, email = ?, phone = ?, address = ?
- WHERE id = ?
- """,
- (
- wallet_id,
- data.currency,
- data.status,
- data.company_name,
- data.first_name,
- data.last_name,
- data.email,
- data.phone,
- data.address,
- data.id,
- ),
- )
-
- invoice = await get_invoice(data.id)
- assert invoice, "Newly updated invoice couldn't be retrieved"
- return invoice
-
-
-async def update_invoice_items(
- invoice_id: str, data: List[UpdateInvoiceItemData]
-) -> List[InvoiceItem]:
- updated_items = []
- for item in data:
- if item.id:
- updated_items.append(item.id)
- await db.execute(
- """
- UPDATE invoices.invoice_items
- SET description = ?, amount = ?
- WHERE id = ?
- """,
- (item.description, int(item.amount * 100), item.id),
- )
-
- placeholders = ",".join("?" for _ in range(len(updated_items)))
- if not placeholders:
- placeholders = "?"
- updated_items = ["skip"]
-
- await db.execute(
- f"""
- DELETE FROM invoices.invoice_items
- WHERE invoice_id = ?
- AND id NOT IN ({placeholders})
- """,
- (
- invoice_id,
- *tuple(updated_items),
- ),
- )
-
- for item in data:
- if not item:
- await create_invoice_items(
- invoice_id=invoice_id,
- data=[CreateInvoiceItemData(description=item.description)],
- )
-
- invoice_items = await get_invoice_items(invoice_id)
- return invoice_items
-
-
-async def create_invoice_payment(invoice_id: str, amount: int) -> Payment:
- payment_id = urlsafe_short_hash()
- await db.execute(
- """
- INSERT INTO invoices.payments (id, invoice_id, amount)
- VALUES (?, ?, ?)
- """,
- (
- payment_id,
- invoice_id,
- amount,
- ),
- )
-
- payment = await get_invoice_payment(payment_id)
- assert payment, "Newly created payment couldn't be retrieved"
- return payment
diff --git a/lnbits/extensions/invoices/migrations.py b/lnbits/extensions/invoices/migrations.py
deleted file mode 100644
index 74a0fdbad..000000000
--- a/lnbits/extensions/invoices/migrations.py
+++ /dev/null
@@ -1,55 +0,0 @@
-async def m001_initial_invoices(db):
-
- # STATUS COLUMN OPTIONS: 'draft', 'open', 'paid', 'canceled'
-
- await db.execute(
- f"""
- CREATE TABLE invoices.invoices (
- id TEXT PRIMARY KEY,
- wallet TEXT NOT NULL,
-
- status TEXT NOT NULL DEFAULT 'draft',
-
- currency TEXT NOT NULL,
-
- company_name TEXT DEFAULT NULL,
- first_name TEXT DEFAULT NULL,
- last_name TEXT DEFAULT NULL,
- email TEXT DEFAULT NULL,
- phone TEXT DEFAULT NULL,
- address TEXT DEFAULT NULL,
-
-
- time TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
- );
- """
- )
-
- await db.execute(
- f"""
- CREATE TABLE invoices.invoice_items (
- id TEXT PRIMARY KEY,
- invoice_id TEXT NOT NULL,
-
- description TEXT NOT NULL,
- amount INTEGER NOT NULL,
-
- FOREIGN KEY(invoice_id) REFERENCES {db.references_schema}invoices(id)
- );
- """
- )
-
- await db.execute(
- f"""
- CREATE TABLE invoices.payments (
- id TEXT PRIMARY KEY,
- invoice_id TEXT NOT NULL,
-
- amount {db.big_int} NOT NULL,
-
- time TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
-
- FOREIGN KEY(invoice_id) REFERENCES {db.references_schema}invoices(id)
- );
- """
- )
diff --git a/lnbits/extensions/invoices/models.py b/lnbits/extensions/invoices/models.py
deleted file mode 100644
index 6f0e63cb5..000000000
--- a/lnbits/extensions/invoices/models.py
+++ /dev/null
@@ -1,104 +0,0 @@
-from enum import Enum
-from sqlite3 import Row
-from typing import List, Optional
-
-from fastapi import Query
-from pydantic import BaseModel
-
-
-class InvoiceStatusEnum(str, Enum):
- draft = "draft"
- open = "open"
- paid = "paid"
- canceled = "canceled"
-
-
-class CreateInvoiceItemData(BaseModel):
- description: str
- amount: float = Query(..., ge=0.01)
-
-
-class CreateInvoiceData(BaseModel):
- status: InvoiceStatusEnum = InvoiceStatusEnum.draft
- currency: str
- company_name: Optional[str]
- first_name: Optional[str]
- last_name: Optional[str]
- email: Optional[str]
- phone: Optional[str]
- address: Optional[str]
- items: List[CreateInvoiceItemData]
-
- class Config:
- use_enum_values = True
-
-
-class UpdateInvoiceItemData(BaseModel):
- id: Optional[str]
- description: str
- amount: float = Query(..., ge=0.01)
-
-
-class UpdateInvoiceData(BaseModel):
- id: str
- wallet: str
- status: InvoiceStatusEnum = InvoiceStatusEnum.draft
- currency: str
- company_name: Optional[str]
- first_name: Optional[str]
- last_name: Optional[str]
- email: Optional[str]
- phone: Optional[str]
- address: Optional[str]
- items: List[UpdateInvoiceItemData]
-
-
-class Invoice(BaseModel):
- id: str
- wallet: str
- status: InvoiceStatusEnum = InvoiceStatusEnum.draft
- currency: str
- company_name: Optional[str]
- first_name: Optional[str]
- last_name: Optional[str]
- email: Optional[str]
- phone: Optional[str]
- address: Optional[str]
- time: int
-
- class Config:
- use_enum_values = True
-
- @classmethod
- def from_row(cls, row: Row) -> "Invoice":
- return cls(**dict(row))
-
-
-class InvoiceItem(BaseModel):
- id: str
- invoice_id: str
- description: str
- amount: int
-
- class Config:
- orm_mode = True
-
- @classmethod
- def from_row(cls, row: Row) -> "InvoiceItem":
- return cls(**dict(row))
-
-
-class Payment(BaseModel):
- id: str
- invoice_id: str
- amount: int
- time: int
-
- @classmethod
- def from_row(cls, row: Row) -> "Payment":
- return cls(**dict(row))
-
-
-class CreatePaymentData(BaseModel):
- invoice_id: str
- amount: int
diff --git a/lnbits/extensions/invoices/static/css/pay.css b/lnbits/extensions/invoices/static/css/pay.css
deleted file mode 100644
index 75ccd112e..000000000
--- a/lnbits/extensions/invoices/static/css/pay.css
+++ /dev/null
@@ -1,67 +0,0 @@
-#invoicePage > .row:first-child > .col-md-6 {
- display: flex;
-}
-
-#invoicePage > .row:first-child > .col-md-6 > .q-card {
- flex: 1;
-}
-
-#invoicePage .clear {
- margin-bottom: 25px;
-}
-
-#printQrCode {
- display: none;
-}
-
-@media (min-width: 1024px) {
- #invoicePage > .row:first-child > .col-md-6:first-child > div {
- margin-right: 5px;
- }
-
- #invoicePage > .row:first-child > .col-md-6:nth-child(2) > div {
- margin-left: 5px;
- }
-}
-
-@media print {
- * {
- color: black !important;
- }
-
- header,
- button,
- #payButtonContainer {
- display: none !important;
- }
-
- main,
- .q-page-container {
- padding-top: 0px !important;
- }
-
- .q-card {
- box-shadow: none !important;
- border: 1px solid black;
- }
-
- .q-item {
- padding: 5px;
- }
-
- .q-card__section {
- padding: 5px;
- }
-
- #printQrCode {
- display: block;
- }
-
- p {
- margin-bottom: 0px !important;
- }
-
- #invoicePage .clear {
- margin-bottom: 10px !important;
- }
-}
diff --git a/lnbits/extensions/invoices/static/image/invoices.png b/lnbits/extensions/invoices/static/image/invoices.png
deleted file mode 100644
index 823f9dee2..000000000
Binary files a/lnbits/extensions/invoices/static/image/invoices.png and /dev/null differ
diff --git a/lnbits/extensions/invoices/tasks.py b/lnbits/extensions/invoices/tasks.py
deleted file mode 100644
index c8a829dba..000000000
--- a/lnbits/extensions/invoices/tasks.py
+++ /dev/null
@@ -1,52 +0,0 @@
-import asyncio
-
-from lnbits.core.models import Payment
-from lnbits.tasks import register_invoice_listener
-
-from .crud import (
- create_invoice_payment,
- get_invoice,
- get_invoice_items,
- get_invoice_payments,
- get_invoice_total,
- get_payments_total,
- update_invoice_internal,
-)
-from .models import InvoiceStatusEnum
-
-
-async def wait_for_paid_invoices():
- invoice_queue = asyncio.Queue()
- register_invoice_listener(invoice_queue)
-
- while True:
- payment = await invoice_queue.get()
- await on_invoice_paid(payment)
-
-
-async def on_invoice_paid(payment: Payment) -> None:
- if payment.extra.get("tag") != "invoices":
- return
-
- invoice_id = payment.extra.get("invoice_id")
- assert invoice_id
-
- amount = payment.extra.get("famount")
- assert amount
-
- await create_invoice_payment(invoice_id=invoice_id, amount=amount)
-
- invoice = await get_invoice(invoice_id)
- assert invoice
-
- invoice_items = await get_invoice_items(invoice_id)
- invoice_total = await get_invoice_total(invoice_items)
-
- invoice_payments = await get_invoice_payments(invoice_id)
- payments_total = await get_payments_total(invoice_payments)
-
- if payments_total >= invoice_total:
- invoice.status = InvoiceStatusEnum.paid
- await update_invoice_internal(invoice.wallet, invoice)
-
- return
diff --git a/lnbits/extensions/invoices/templates/invoices/_api_docs.html b/lnbits/extensions/invoices/templates/invoices/_api_docs.html
deleted file mode 100644
index 6e2a63554..000000000
--- a/lnbits/extensions/invoices/templates/invoices/_api_docs.html
+++ /dev/null
@@ -1,153 +0,0 @@
-GET /invoices/api/v1/invoices
- Headers
- {"X-Api-Key": <invoice_key>}
- Body (application/json)
-
- Returns 200 OK (application/json)
-
- [<invoice_object>, ...]
- Curl example
- curl -X GET {{ request.base_url }}invoices/api/v1/invoices -H
- "X-Api-Key: <invoice_key>"
-
- GET
- /invoices/api/v1/invoice/{invoice_id}
- Headers
- {"X-Api-Key": <invoice_key>}
- Body (application/json)
-
- Returns 200 OK (application/json)
-
- {invoice_object}
- Curl example
- curl -X GET {{ request.base_url
- }}invoices/api/v1/invoice/{invoice_id} -H "X-Api-Key:
- <invoice_key>"
-
- POST /invoices/api/v1/invoice
- Headers
- {"X-Api-Key": <invoice_key>}
- Body (application/json)
-
- Returns 200 OK (application/json)
-
- {invoice_object}
- Curl example
- curl -X POST {{ request.base_url }}invoices/api/v1/invoice -H
- "X-Api-Key: <invoice_key>"
-
- POST
- /invoices/api/v1/invoice/{invoice_id}
- Headers
- {"X-Api-Key": <invoice_key>}
- Body (application/json)
-
- Returns 200 OK (application/json)
-
- {invoice_object}
- Curl example
- curl -X POST {{ request.base_url
- }}invoices/api/v1/invoice/{invoice_id} -H "X-Api-Key:
- <invoice_key>"
-
- POST
- /invoices/api/v1/invoice/{invoice_id}/payments
- Headers
- Body (application/json)
-
- Returns 200 OK (application/json)
-
- {payment_object}
- Curl example
- curl -X POST {{ request.base_url
- }}invoices/api/v1/invoice/{invoice_id}/payments -H "X-Api-Key:
- <invoice_key>"
-
- GET
- /invoices/api/v1/invoice/{invoice_id}/payments/{payment_hash}
- Headers
- Body (application/json)
-
- Returns 200 OK (application/json)
-
- Curl example
- curl -X GET {{ request.base_url
- }}invoices/api/v1/invoice/{invoice_id}/payments/{payment_hash} -H
- "X-Api-Key: <invoice_key>"
-
-
- Invoice -
- -- Bill To -
- -- Items -
- -- Payments -
- -Scan to View & Pay Online!
-{{ request.url }}
-