feat: code quality
format add ci fixup ci
This commit is contained in:
parent
09bb033f85
commit
42b5edaf5d
26 changed files with 3199 additions and 295 deletions
34
.github/workflows/ci.yml
vendored
Normal file
34
.github/workflows/ci.yml
vendored
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
name: CI
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
uses: lnbits/lnbits/.github/workflows/lint.yml@dev
|
||||
tests:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [lint]
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ['3.9', '3.10']
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: lnbits/lnbits/.github/actions/prepare@dev
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Run pytest
|
||||
uses: pavelzw/pytest-action@v2
|
||||
env:
|
||||
LNBITS_BACKEND_WALLET_CLASS: FakeWallet
|
||||
PYTHONUNBUFFERED: 1
|
||||
DEBUG: true
|
||||
with:
|
||||
verbose: true
|
||||
job-summary: true
|
||||
emoji: false
|
||||
click-to-expand: true
|
||||
custom-pytest: poetry run pytest
|
||||
report-title: 'test (${{ matrix.python-version }})'
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -1 +1,4 @@
|
|||
__pycache__
|
||||
node_modules
|
||||
.mypy_cache
|
||||
.venv
|
||||
|
|
|
|||
12
.prettierrc
Normal file
12
.prettierrc
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"semi": false,
|
||||
"arrowParens": "avoid",
|
||||
"insertPragma": false,
|
||||
"printWidth": 80,
|
||||
"proseWrap": "preserve",
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"useTabs": false,
|
||||
"bracketSameLine": false,
|
||||
"bracketSpacing": false
|
||||
}
|
||||
47
Makefile
Normal file
47
Makefile
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
all: format check
|
||||
|
||||
format: prettier black ruff
|
||||
|
||||
check: mypy pyright checkblack checkruff checkprettier
|
||||
|
||||
prettier:
|
||||
poetry run ./node_modules/.bin/prettier --write .
|
||||
pyright:
|
||||
poetry run ./node_modules/.bin/pyright
|
||||
|
||||
mypy:
|
||||
poetry run mypy .
|
||||
|
||||
black:
|
||||
poetry run black .
|
||||
|
||||
ruff:
|
||||
poetry run ruff check . --fix
|
||||
|
||||
checkruff:
|
||||
poetry run ruff check .
|
||||
|
||||
checkprettier:
|
||||
poetry run ./node_modules/.bin/prettier --check .
|
||||
|
||||
checkblack:
|
||||
poetry run black --check .
|
||||
|
||||
checkeditorconfig:
|
||||
editorconfig-checker
|
||||
|
||||
test:
|
||||
PYTHONUNBUFFERED=1 \
|
||||
DEBUG=true \
|
||||
poetry run pytest
|
||||
install-pre-commit-hook:
|
||||
@echo "Installing pre-commit hook to git"
|
||||
@echo "Uninstall the hook with poetry run pre-commit uninstall"
|
||||
poetry run pre-commit install
|
||||
|
||||
pre-commit:
|
||||
poetry run pre-commit run --all-files
|
||||
|
||||
|
||||
checkbundle:
|
||||
@echo "skipping checkbundle"
|
||||
|
|
@ -7,6 +7,7 @@
|
|||
Ready to start hacking? Once you've forked this extension, you can incorporate functions from other extensions as needed.
|
||||
|
||||
### How to Use This Template
|
||||
|
||||
> This guide assumes you're using this extension as a base for a new one, and have installed LNbits using https://github.com/lnbits/lnbits/blob/main/docs/guide/installation.md#option-1-recommended-poetry.
|
||||
|
||||
1. Install and enable the extension either through the official LNbits manifest or by adding https://raw.githubusercontent.com/lnbits/myextension/main/manifest.json to `"Server"/"Server"/"Extension Sources"`.  
|
||||
|
|
|
|||
38
__init__.py
38
__init__.py
|
|
@ -1,19 +1,24 @@
|
|||
import asyncio
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
from lnbits.db import Database
|
||||
from lnbits.helpers import template_renderer
|
||||
from lnbits.tasks import create_permanent_unique_task
|
||||
from loguru import logger
|
||||
|
||||
from .crud import db
|
||||
from .tasks import wait_for_paid_invoices
|
||||
from .views import myextension_generic_router
|
||||
from .views_api import myextension_api_router
|
||||
from .views_lnurl import myextension_lnurl_router
|
||||
|
||||
logger.debug(
|
||||
"This logged message is from myextension/__init__.py, you can debug in your extension using 'import logger from loguru' and 'logger.debug(<thing-to-log>)'."
|
||||
"This logged message is from myextension/__init__.py, you can debug in your "
|
||||
"extension using 'import logger from loguru' and 'logger.debug(<thing-to-log>)'."
|
||||
)
|
||||
|
||||
db = Database("ext_myextension")
|
||||
|
||||
myextension_ext: APIRouter = APIRouter(prefix="/myextension", tags=["MyExtension"])
|
||||
myextension_ext.include_router(myextension_generic_router)
|
||||
myextension_ext.include_router(myextension_api_router)
|
||||
myextension_ext.include_router(myextension_lnurl_router)
|
||||
|
||||
myextension_static_files = [
|
||||
{
|
||||
|
|
@ -22,16 +27,6 @@ myextension_static_files = [
|
|||
}
|
||||
]
|
||||
|
||||
|
||||
def myextension_renderer():
|
||||
return template_renderer(["myextension/templates"])
|
||||
|
||||
|
||||
from .lnurl import *
|
||||
from .tasks import wait_for_paid_invoices
|
||||
from .views import *
|
||||
from .views_api import *
|
||||
|
||||
scheduled_tasks: list[asyncio.Task] = []
|
||||
|
||||
|
||||
|
|
@ -44,5 +39,16 @@ def myextension_stop():
|
|||
|
||||
|
||||
def myextension_start():
|
||||
from lnbits.tasks import create_permanent_unique_task
|
||||
|
||||
task = create_permanent_unique_task("ext_myextension", wait_for_paid_invoices)
|
||||
scheduled_tasks.append(task)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"db",
|
||||
"myextension_ext",
|
||||
"myextension_static_files",
|
||||
"myextension_start",
|
||||
"myextension_stop",
|
||||
]
|
||||
|
|
|
|||
128
crud.py
128
crud.py
|
|
@ -1,99 +1,77 @@
|
|||
from typing import List, Optional, Union
|
||||
from typing import Optional, Union
|
||||
|
||||
from lnbits.helpers import urlsafe_short_hash
|
||||
from lnbits.lnurl import encode as lnurl_encode
|
||||
from . import db
|
||||
from .models import CreateMyExtensionData, MyExtension
|
||||
from fastapi import Request
|
||||
from lnurl import encode as lnurl_encode
|
||||
import shortuuid
|
||||
from lnbits.db import Database
|
||||
from lnbits.helpers import insert_query, update_query
|
||||
|
||||
from .models import MyExtension
|
||||
|
||||
db = Database("ext_myextension")
|
||||
table_name = "myextension.maintable"
|
||||
|
||||
|
||||
async def create_myextension(
|
||||
wallet_id: str, data: CreateMyExtensionData, req: Request
|
||||
) -> MyExtension:
|
||||
myextension_id = urlsafe_short_hash()
|
||||
async def create_myextension(data: MyExtension) -> MyExtension:
|
||||
await db.execute(
|
||||
"""
|
||||
INSERT INTO myextension.maintable (id, wallet, name, lnurlpayamount, lnurlwithdrawamount)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
myextension_id,
|
||||
wallet_id,
|
||||
data.name,
|
||||
data.lnurlpayamount,
|
||||
data.lnurlwithdrawamount,
|
||||
),
|
||||
insert_query(table_name, data),
|
||||
(*data.dict().values(),),
|
||||
)
|
||||
myextension = await get_myextension(myextension_id, req)
|
||||
assert myextension, "Newly created table couldn't be retrieved"
|
||||
return myextension
|
||||
return data
|
||||
|
||||
# this is how we used to do it
|
||||
|
||||
# myextension_id = urlsafe_short_hash()
|
||||
# await db.execute(
|
||||
# """
|
||||
# INSERT INTO myextension.maintable
|
||||
# (id, wallet, name, lnurlpayamount, lnurlwithdrawamount)
|
||||
# VALUES (?, ?, ?, ?, ?)
|
||||
# """,
|
||||
# (
|
||||
# myextension_id,
|
||||
# wallet_id,
|
||||
# data.name,
|
||||
# data.lnurlpayamount,
|
||||
# data.lnurlwithdrawamount,
|
||||
# ),
|
||||
# )
|
||||
# myextension = await get_myextension(myextension_id)
|
||||
# assert myextension, "Newly created table couldn't be retrieved"
|
||||
|
||||
|
||||
async def get_myextension(
|
||||
myextension_id: str, req: Optional[Request] = None
|
||||
) -> Optional[MyExtension]:
|
||||
async def get_myextension(myextension_id: str) -> Optional[MyExtension]:
|
||||
row = await db.fetchone(
|
||||
"SELECT * FROM myextension.maintable WHERE id = ?", (myextension_id,)
|
||||
f"SELECT * FROM {table_name} WHERE id = ?", (myextension_id,)
|
||||
)
|
||||
if not row:
|
||||
return None
|
||||
rowAmended = MyExtension(**row)
|
||||
if req:
|
||||
rowAmended.lnurlpay = lnurl_encode(
|
||||
req.url_for("myextension.api_lnurl_pay", myextension_id=row.id)._url
|
||||
)
|
||||
rowAmended.lnurlwithdraw = lnurl_encode(
|
||||
req.url_for(
|
||||
"myextension.api_lnurl_withdraw",
|
||||
myextension_id=row.id,
|
||||
tickerhash=shortuuid.uuid(name=rowAmended.id + str(rowAmended.ticker)),
|
||||
)._url
|
||||
)
|
||||
return rowAmended
|
||||
return MyExtension(**row) if row else None
|
||||
|
||||
|
||||
async def get_myextensions(
|
||||
wallet_ids: Union[str, List[str]], req: Optional[Request] = None
|
||||
) -> List[MyExtension]:
|
||||
async def get_myextensions(wallet_ids: Union[str, list[str]]) -> list[MyExtension]:
|
||||
if isinstance(wallet_ids, str):
|
||||
wallet_ids = [wallet_ids]
|
||||
|
||||
q = ",".join(["?"] * len(wallet_ids))
|
||||
rows = await db.fetchall(
|
||||
f"SELECT * FROM myextension.maintable WHERE wallet IN ({q})", (*wallet_ids,)
|
||||
f"SELECT * FROM {table_name} WHERE wallet IN ({q})", (*wallet_ids,)
|
||||
)
|
||||
tempRows = [MyExtension(**row) for row in rows]
|
||||
if req:
|
||||
for row in tempRows:
|
||||
row.lnurlpay = lnurl_encode(
|
||||
req.url_for("myextension.api_lnurl_pay", myextension_id=row.id)._url
|
||||
)
|
||||
row.lnurlwithdraw = lnurl_encode(
|
||||
req.url_for(
|
||||
"myextension.api_lnurl_withdraw",
|
||||
myextension_id=row.id,
|
||||
tickerhash=shortuuid.uuid(name=row.id + str(row.ticker)),
|
||||
)._url
|
||||
)
|
||||
return tempRows
|
||||
return [MyExtension(**row) for row in rows]
|
||||
|
||||
|
||||
async def update_myextension(
|
||||
myextension_id: str, req: Optional[Request] = None, **kwargs
|
||||
) -> MyExtension:
|
||||
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
|
||||
async def update_myextension(data: MyExtension) -> MyExtension:
|
||||
await db.execute(
|
||||
f"UPDATE myextension.maintable SET {q} WHERE id = ?",
|
||||
(*kwargs.values(), myextension_id),
|
||||
update_query(table_name, data),
|
||||
(
|
||||
*data.dict().values(),
|
||||
data.id,
|
||||
),
|
||||
)
|
||||
myextension = await get_myextension(myextension_id, req)
|
||||
assert myextension, "Newly updated myextension couldn't be retrieved"
|
||||
return myextension
|
||||
return data
|
||||
# this is how we used to do it
|
||||
|
||||
# q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
|
||||
# await db.execute(
|
||||
# f"UPDATE myextension.maintable SET {q} WHERE id = ?",
|
||||
# (*kwargs.values(), myextension_id),
|
||||
# )
|
||||
|
||||
|
||||
async def delete_myextension(myextension_id: str) -> None:
|
||||
await db.execute(
|
||||
"DELETE FROM myextension.maintable WHERE id = ?", (myextension_id,)
|
||||
)
|
||||
await db.execute(f"DELETE FROM {table_name} WHERE id = ?", (myextension_id,))
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ This is a longform description that will be used in the advanced description whe
|
|||
|
||||
Adding some bullets is nice covering:
|
||||
|
||||
* Functionality
|
||||
* Use cases
|
||||
- Functionality
|
||||
- Use cases
|
||||
|
||||
...and some other text about just how great this etension is.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
# the migration file is where you build your database tables
|
||||
# If you create a new release for your extension , remeember the migration file is like a blockchain, never edit only add!
|
||||
# If you create a new release for your extension ,
|
||||
# remember the migration file is like a blockchain, never edit only add!
|
||||
|
||||
|
||||
async def m001_initial(db):
|
||||
|
|
@ -20,17 +21,3 @@ async def m001_initial(db):
|
|||
);
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
# Here we add another field to the database
|
||||
|
||||
|
||||
async def m002_addtip_wallet(db):
|
||||
"""
|
||||
Add total to templates table
|
||||
"""
|
||||
await db.execute(
|
||||
"""
|
||||
ALTER TABLE myextension.maintable ADD ticker INTEGER DEFAULT 1;
|
||||
"""
|
||||
)
|
||||
|
|
|
|||
28
models.py
28
models.py
|
|
@ -1,30 +1,24 @@
|
|||
# Data models for your extension
|
||||
|
||||
from sqlite3 import Row
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class CreateMyExtensionData(BaseModel):
|
||||
wallet: Optional[str]
|
||||
name: Optional[str]
|
||||
total: Optional[int]
|
||||
lnurlpayamount: Optional[int]
|
||||
lnurlwithdrawamount: Optional[int]
|
||||
ticker: Optional[int]
|
||||
name: str
|
||||
lnurlpayamount: int
|
||||
lnurlwithdrawamount: int
|
||||
wallet: Optional[str] = None
|
||||
total: int = 0
|
||||
|
||||
|
||||
class MyExtension(BaseModel):
|
||||
id: str
|
||||
wallet: Optional[str]
|
||||
name: Optional[str]
|
||||
total: Optional[int]
|
||||
lnurlpayamount: Optional[int]
|
||||
lnurlwithdrawamount: Optional[int]
|
||||
wallet: str
|
||||
lnurlpayamount: int
|
||||
name: str
|
||||
lnurlwithdrawamount: int
|
||||
total: int
|
||||
lnurlpay: Optional[str]
|
||||
lnurlwithdraw: Optional[str]
|
||||
ticker: Optional[int]
|
||||
|
||||
@classmethod
|
||||
def from_row(cls, row: Row) -> "MyExtension":
|
||||
return cls(**dict(row))
|
||||
|
|
|
|||
59
package-lock.json
generated
Normal file
59
package-lock.json
generated
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
{
|
||||
"name": "lnurlp",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "lnurlp",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"prettier": "^3.2.5",
|
||||
"pyright": "^1.1.358"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz",
|
||||
"integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==",
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/pyright": {
|
||||
"version": "1.1.375",
|
||||
"resolved": "https://registry.npmjs.org/pyright/-/pyright-1.1.375.tgz",
|
||||
"integrity": "sha512-DeSxwNWSFXPr079RFtvUW8O30XC80tuQf7KLMlmj5aks7isDEzcjSQkvMKiXd+a4Ueo+JTq0WsDlEoN/2BYKJA==",
|
||||
"bin": {
|
||||
"pyright": "index.js",
|
||||
"pyright-langserver": "langserver.index.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
15
package.json
Normal file
15
package.json
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"name": "lnurlp",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"prettier": "^3.2.5",
|
||||
"pyright": "^1.1.358"
|
||||
}
|
||||
}
|
||||
2534
poetry.lock
generated
Normal file
2534
poetry.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
89
pyproject.toml
Normal file
89
pyproject.toml
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
[tool.poetry]
|
||||
name = "myextension"
|
||||
version = "0.0.0"
|
||||
description = "Eightball is a simple API that allows you to create a random number generator."
|
||||
authors = ["benarc", "dni <dni@lnbits.com>"]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.10 | ^3.9"
|
||||
lnbits = "*"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
black = "^24.3.0"
|
||||
pytest-asyncio = "^0.21.0"
|
||||
pytest = "^7.3.2"
|
||||
mypy = "^1.5.1"
|
||||
pre-commit = "^3.2.2"
|
||||
ruff = "^0.3.2"
|
||||
pytest-md = "^0.2.0"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.0.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.mypy]
|
||||
[[tool.mypy.overrides]]
|
||||
module = [
|
||||
"lnbits.*",
|
||||
"loguru.*",
|
||||
"fastapi.*",
|
||||
"pydantic.*",
|
||||
]
|
||||
ignore_missing_imports = "True"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
log_cli = false
|
||||
testpaths = [
|
||||
"tests"
|
||||
]
|
||||
|
||||
[tool.black]
|
||||
line-length = 88
|
||||
|
||||
[tool.ruff]
|
||||
# Same as Black. + 10% rule of black
|
||||
line-length = 88
|
||||
|
||||
|
||||
[tool.ruff.lint]
|
||||
# Enable:
|
||||
# F - pyflakes
|
||||
# E - pycodestyle errors
|
||||
# W - pycodestyle warnings
|
||||
# I - isort
|
||||
# A - flake8-builtins
|
||||
# C - mccabe
|
||||
# N - naming
|
||||
# UP - pyupgrade
|
||||
# RUF - ruff
|
||||
# B - bugbear
|
||||
select = ["F", "E", "W", "I", "A", "C", "N", "UP", "RUF", "B"]
|
||||
# UP007: pyupgrade: use X | Y instead of Optional. (python3.10)
|
||||
ignore = ["UP007"]
|
||||
|
||||
# Allow autofix for all enabled rules (when `--fix`) is provided.
|
||||
fixable = ["ALL"]
|
||||
unfixable = []
|
||||
|
||||
# Allow unused variables when underscore-prefixed.
|
||||
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
|
||||
|
||||
# needed for pydantic
|
||||
[tool.ruff.lint.pep8-naming]
|
||||
classmethod-decorators = [
|
||||
"root_validator",
|
||||
]
|
||||
|
||||
# Ignore unused imports in __init__.py files.
|
||||
# [tool.ruff.lint.extend-per-file-ignores]
|
||||
# "views_api.py" = ["F401"]
|
||||
|
||||
# [tool.ruff.lint.mccabe]
|
||||
# max-complexity = 10
|
||||
|
||||
[tool.ruff.lint.flake8-bugbear]
|
||||
# Allow default arguments like, e.g., `data: List[str] = fastapi.Query(None)`.
|
||||
extend-immutable-calls = [
|
||||
"fastapi.Depends",
|
||||
"fastapi.Query",
|
||||
]
|
||||
12
tasks.py
12
tasks.py
|
|
@ -7,7 +7,6 @@ from lnbits.tasks import register_invoice_listener
|
|||
|
||||
from .crud import get_myextension, update_myextension
|
||||
|
||||
|
||||
#######################################
|
||||
########## RUN YOUR TASKS HERE ########
|
||||
#######################################
|
||||
|
|
@ -31,19 +30,22 @@ async def on_invoice_paid(payment: Payment) -> None:
|
|||
return
|
||||
|
||||
myextension_id = payment.extra.get("myextensionId")
|
||||
assert myextension_id, "myextensionId not set in invoice"
|
||||
myextension = await get_myextension(myextension_id)
|
||||
assert myextension, "MyExtension does not exist"
|
||||
|
||||
# update something in the db
|
||||
if payment.extra.get("lnurlwithdraw"):
|
||||
total = myextension.total - payment.amount
|
||||
else:
|
||||
total = myextension.total + payment.amount
|
||||
data_to_update = {"total": total}
|
||||
|
||||
await update_myextension(myextension_id=myextension_id, **data_to_update)
|
||||
myextension.total = total
|
||||
await update_myextension(myextension)
|
||||
|
||||
# here we could send some data to a websocket on wss://<your-lnbits>/api/v1/ws/<myextension_id>
|
||||
# and then listen to it on the frontend, which we do with index.html connectWebocket()
|
||||
# here we could send some data to a websocket on
|
||||
# wss://<your-lnbits>/api/v1/ws/<myextension_id> and then listen to it on
|
||||
# the frontend, which we do with index.html connectWebocket()
|
||||
|
||||
some_payment_data = {
|
||||
"name": myextension.name,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,8 @@
|
|||
<q-expansion-item group="extras" icon="swap_vertical_circle" label="API info" :content-inset-level="0.5">
|
||||
<q-expansion-item
|
||||
group="extras"
|
||||
icon="swap_vertical_circle"
|
||||
label="API info"
|
||||
:content-inset-level="0.5"
|
||||
>
|
||||
<q-btn flat label="Swagger API" type="a" href="../docs#/MyExtension"></q-btn>
|
||||
</q-expansion-item>
|
||||
|
|
@ -1,13 +1,25 @@
|
|||
<q-expansion-item group="extras" icon="info" label="More info">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<p>
|
||||
Some more info about my excellent extension.
|
||||
</p>
|
||||
<small>Created by
|
||||
<a class="text-secondary" href="https://github.com/benarc" target="_blank">Ben Arc</a>.</small>
|
||||
<small>Repo
|
||||
<a class="text-secondary" href="https://github.com/lnbits/myextension" target="_blank">MyExtension</a>.</small>
|
||||
<p>Some more info about my excellent extension.</p>
|
||||
<small
|
||||
>Created by
|
||||
<a
|
||||
class="text-secondary"
|
||||
href="https://github.com/benarc"
|
||||
target="_blank"
|
||||
>Ben Arc</a
|
||||
>.</small
|
||||
>
|
||||
<small
|
||||
>Repo
|
||||
<a
|
||||
class="text-secondary"
|
||||
href="https://github.com/lnbits/myextension"
|
||||
target="_blank"
|
||||
>MyExtension</a
|
||||
>.</small
|
||||
>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
|
|
@ -8,7 +8,9 @@
|
|||
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<q-btn unelevated color="primary" @click="formDialog.show = true">New MyExtension</q-btn>
|
||||
<q-btn unelevated color="primary" @click="formDialog.show = true"
|
||||
>New MyExtension</q-btn
|
||||
>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
|
|
@ -22,8 +24,14 @@
|
|||
<q-btn flat color="grey" @click="exportCSV">Export to CSV</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
<q-table dense flat :data="myex" row-key="id" :columns="myexTable.columns"
|
||||
:pagination.sync="myexTable.pagination">
|
||||
<q-table
|
||||
dense
|
||||
flat
|
||||
:data="myex"
|
||||
row-key="id"
|
||||
:columns="myexTable.columns"
|
||||
:pagination.sync="myexTable.pagination"
|
||||
>
|
||||
<myextension v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
||||
|
|
@ -39,28 +47,57 @@
|
|||
<div v-else>${ col.value }</div>
|
||||
</q-td>
|
||||
<q-td auto-width>
|
||||
<q-btn unelevated dense size="sm" icon="qr_code" :color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||
class="q-mr-sm" @click="openUrlDialog(props.row.id)"></q-btn></q-td>
|
||||
<q-btn
|
||||
unelevated
|
||||
dense
|
||||
size="sm"
|
||||
icon="qr_code"
|
||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||
class="q-mr-sm"
|
||||
@click="openUrlDialog(props.row.id)"
|
||||
></q-btn
|
||||
></q-td>
|
||||
<q-td auto-width>
|
||||
<q-btn unelevated dense size="sm" icon="launch" :color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||
type="a" :href="props.row.myextension" target="_blank"><q-tooltip>Open public
|
||||
page</q-tooltip></q-btn></q-td>
|
||||
<q-btn
|
||||
unelevated
|
||||
dense
|
||||
size="sm"
|
||||
icon="launch"
|
||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||
type="a"
|
||||
:href="props.row.myextension"
|
||||
target="_blank"
|
||||
><q-tooltip>Open public page</q-tooltip></q-btn
|
||||
></q-td
|
||||
>
|
||||
|
||||
<q-td>
|
||||
<q-btn flat dense size="xs" @click="updateMyExtensionForm(props.row.id)" icon="edit" color="light-blue">
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
size="xs"
|
||||
@click="updateMyExtensionForm(props.row.id)"
|
||||
icon="edit"
|
||||
color="light-blue"
|
||||
>
|
||||
<q-tooltip> Edit copilot </q-tooltip>
|
||||
</q-btn>
|
||||
</q-td>
|
||||
|
||||
<q-td>
|
||||
<q-btn flat dense size="xs" @click="deleteMyExtension(props.row.id)" icon="cancel" color="pink">
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
size="xs"
|
||||
@click="deleteMyExtension(props.row.id)"
|
||||
icon="cancel"
|
||||
color="pink"
|
||||
>
|
||||
<q-tooltip> Delete copilot </q-tooltip>
|
||||
</q-btn>
|
||||
</q-td>
|
||||
|
||||
</q-tr>
|
||||
</template>
|
||||
|
||||
</q-table>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
|
@ -69,9 +106,13 @@
|
|||
<div class="col-12 col-md-4 col-lg-5 q-gutter-y-md">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<h6 class="text-subtitle1 q-my-none">{{SITE_TITLE}} MyExtension extension</h6>
|
||||
<p>Simple extension you can use as a base for your own extension. <br /> Includes very simple LNURL-pay and
|
||||
LNURL-withdraw example.</p>
|
||||
<h6 class="text-subtitle1 q-my-none">
|
||||
{{SITE_TITLE}} MyExtension extension
|
||||
</h6>
|
||||
<p>
|
||||
Simple extension you can use as a base for your own extension. <br />
|
||||
Includes very simple LNURL-pay and LNURL-withdraw example.
|
||||
</p>
|
||||
</q-card-section>
|
||||
<q-card-section class="q-pa-none">
|
||||
<q-separator></q-separator>
|
||||
|
|
@ -91,20 +132,54 @@
|
|||
<q-dialog v-model="formDialog.show" position="top" @hide="closeFormDialog">
|
||||
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
|
||||
<q-form @submit="sendMyExtensionData" class="q-gutter-md">
|
||||
<q-input filled dense v-model.trim="formDialog.data.name" label="Name"
|
||||
placeholder="Name for your record"></q-input>
|
||||
<q-select filled dense emit-value v-model="formDialog.data.wallet" :options="g.user.walletOptions"
|
||||
label="Wallet *"></q-select>
|
||||
<q-input filled dense type="number" v-model.trim="formDialog.data.lnurlwithdrawamount"
|
||||
label="LNURL-withdraw amount"></q-input>
|
||||
<q-input filled dense type="number" v-model.trim="formDialog.data.lnurlpayamount"
|
||||
label="LNURL-pay amount"></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialog.data.name"
|
||||
label="Name"
|
||||
placeholder="Name for your record"
|
||||
></q-input>
|
||||
<q-select
|
||||
filled
|
||||
dense
|
||||
emit-value
|
||||
v-model="formDialog.data.wallet"
|
||||
:options="g.user.walletOptions"
|
||||
label="Wallet *"
|
||||
></q-select>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
type="number"
|
||||
v-model.trim="formDialog.data.lnurlwithdrawamount"
|
||||
label="LNURL-withdraw amount"
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
type="number"
|
||||
v-model.trim="formDialog.data.lnurlpayamount"
|
||||
label="LNURL-pay amount"
|
||||
></q-input>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn v-if="formDialog.data.id" unelevated color="primary" type="submit">Update MyExtension</q-btn>
|
||||
<q-btn v-else unelevated color="primary"
|
||||
<q-btn
|
||||
v-if="formDialog.data.id"
|
||||
unelevated
|
||||
color="primary"
|
||||
type="submit"
|
||||
>Update MyExtension</q-btn
|
||||
>
|
||||
<q-btn
|
||||
v-else
|
||||
unelevated
|
||||
color="primary"
|
||||
:disable="formDialog.data.name == null || formDialog.data.wallet == null || formDialog.data.lnurlwithdrawamount == null || formDialog.data.lnurlpayamount == null"
|
||||
type="submit">Create MyExtension</q-btn>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
|
||||
type="submit"
|
||||
>Create MyExtension</q-btn
|
||||
>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
||||
>Cancel</q-btn
|
||||
>
|
||||
</div>
|
||||
</q-form>
|
||||
</q-card>
|
||||
|
|
@ -119,29 +194,39 @@
|
|||
<q-responsive :ratio="1" class="q-mx-xl q-mb-md">
|
||||
<lnbits-qrcode :value="qrValue"></lnbits-qrcode>
|
||||
</q-responsive>
|
||||
<center><q-btn label="copy" @click="copyText(qrValue)"></q-btn>
|
||||
</center>
|
||||
<center><q-btn label="copy" @click="copyText(qrValue)"></q-btn></center>
|
||||
|
||||
<q-separator></q-separator>
|
||||
|
||||
<div class="row justify-start q-mt-lg">
|
||||
<div class="col col-md-auto">
|
||||
<q-btn outline style="color: primmary;" @click="qrValue = urlDialog.data.lnurlpay">lnurlpay</q-btn>
|
||||
|
||||
<q-btn
|
||||
outline
|
||||
style="color: primmary"
|
||||
@click="qrValue = urlDialog.data.lnurlpay"
|
||||
>lnurlpay</q-btn
|
||||
>
|
||||
</div>
|
||||
<div class="col col-md-auto">
|
||||
<q-btn outline style="color: primmary;" @click="qrValue = urlDialog.data.lnurlwithdraw">lnurlwithdraw</q-btn>
|
||||
|
||||
<q-btn
|
||||
outline
|
||||
style="color: primmary"
|
||||
@click="qrValue = urlDialog.data.lnurlwithdraw"
|
||||
>lnurlwithdraw</q-btn
|
||||
>
|
||||
</div>
|
||||
<div class="col q-pl-md">
|
||||
<q-input filled bottom-slots dense v-model="invoiceAmount">
|
||||
<template v-slot:append>
|
||||
<q-btn round @click="createInvoice(urlDialog.data.wallet, urlDialog.data.id)" color="primary" flat
|
||||
icon="add_circle" />
|
||||
</template>
|
||||
<template v-slot:hint>
|
||||
Create an invoice
|
||||
<q-btn
|
||||
round
|
||||
@click="createInvoice(urlDialog.data.wallet, urlDialog.data.id)"
|
||||
color="primary"
|
||||
flat
|
||||
icon="add_circle"
|
||||
/>
|
||||
</template>
|
||||
<template v-slot:hint> Create an invoice </template>
|
||||
</q-input>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -150,12 +235,10 @@
|
|||
</div>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
</div>
|
||||
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
||||
<script src="https://cdn.jsdelivr.net/npm/canvas-confetti@1.4.0/dist/confetti.browser.min.js"></script>
|
||||
<script>
|
||||
|
||||
///////////////////////////////////////////////////
|
||||
//////////an object we can update with data////////
|
||||
///////////////////////////////////////////////////
|
||||
|
|
@ -178,15 +261,20 @@
|
|||
myex: [],
|
||||
myexTable: {
|
||||
columns: [
|
||||
{ name: 'id', align: 'left', label: 'ID', field: 'id' },
|
||||
{ name: 'name', align: 'left', label: 'Name', field: 'name' },
|
||||
{name: 'id', align: 'left', label: 'ID', field: 'id'},
|
||||
{name: 'name', align: 'left', label: 'Name', field: 'name'},
|
||||
{
|
||||
name: 'wallet',
|
||||
align: 'left',
|
||||
label: 'Wallet',
|
||||
field: 'wallet'
|
||||
},
|
||||
{ name: 'total', align: 'left', label: 'Total sent/received', field: 'total' },
|
||||
{
|
||||
name: 'total',
|
||||
align: 'left',
|
||||
label: 'Total sent/received',
|
||||
field: 'total'
|
||||
}
|
||||
],
|
||||
pagination: {
|
||||
rowsPerPage: 10
|
||||
|
|
@ -247,7 +335,7 @@
|
|||
}
|
||||
},
|
||||
updateMyExtensionForm(tempId) {
|
||||
const myextension = _.findWhere(this.myex, { id: tempId })
|
||||
const myextension = _.findWhere(this.myex, {id: tempId})
|
||||
this.formDialog.data = {
|
||||
...myextension
|
||||
}
|
||||
|
|
@ -291,7 +379,7 @@
|
|||
},
|
||||
deleteMyExtension: function (tempId) {
|
||||
var self = this
|
||||
var myextension = _.findWhere(this.myex, { id: tempId })
|
||||
var myextension = _.findWhere(this.myex, {id: tempId})
|
||||
|
||||
LNbits.utils
|
||||
.confirmDialog('Are you sure you want to delete this MyExtension?')
|
||||
|
|
@ -300,7 +388,8 @@
|
|||
.request(
|
||||
'DELETE',
|
||||
'/myextension/api/v1/myex/' + tempId,
|
||||
_.findWhere(self.g.user.wallets, { id: myextension.wallet }).adminkey
|
||||
_.findWhere(self.g.user.wallets, {id: myextension.wallet})
|
||||
.adminkey
|
||||
)
|
||||
.then(function (response) {
|
||||
self.myex = _.reject(self.myex, function (obj) {
|
||||
|
|
@ -316,12 +405,12 @@
|
|||
LNbits.utils.exportCSV(this.myexTable.columns, this.myex)
|
||||
},
|
||||
itemsArray(tempId) {
|
||||
const myextension = _.findWhere(this.myex, { id: tempId })
|
||||
const myextension = _.findWhere(this.myex, {id: tempId})
|
||||
return [...myextension.itemsMap.values()]
|
||||
},
|
||||
openformDialog(id) {
|
||||
const [tempId, itemId] = id.split(':')
|
||||
const myextension = _.findWhere(this.myex, { id: tempId })
|
||||
const myextension = _.findWhere(this.myex, {id: tempId})
|
||||
if (itemId) {
|
||||
const item = myextension.itemsMap.get(id)
|
||||
this.formDialog.data = {
|
||||
|
|
@ -339,7 +428,7 @@
|
|||
this.formDialog.data = {}
|
||||
},
|
||||
openUrlDialog(id) {
|
||||
this.urlDialog.data = _.findWhere(this.myex, { id })
|
||||
this.urlDialog.data = _.findWhere(this.myex, {id})
|
||||
this.qrValue = this.urlDialog.data.lnurlpay
|
||||
console.log(this.urlDialog.data.id)
|
||||
this.connectWebocket(this.urlDialog.data.id)
|
||||
|
|
@ -363,12 +452,7 @@
|
|||
}
|
||||
}
|
||||
LNbits.api
|
||||
.request(
|
||||
'POST',
|
||||
`/api/v1/payments`,
|
||||
wallet.inkey,
|
||||
dataToSend
|
||||
)
|
||||
.request('POST', `/api/v1/payments`, wallet.inkey, dataToSend)
|
||||
.then(response => {
|
||||
this.qrValue = response.data.payment_request
|
||||
})
|
||||
|
|
@ -377,15 +461,15 @@
|
|||
})
|
||||
},
|
||||
makeItRain() {
|
||||
document.getElementById("vue").disabled = true
|
||||
var end = Date.now() + (2 * 1000)
|
||||
document.getElementById('vue').disabled = true
|
||||
var end = Date.now() + 2 * 1000
|
||||
var colors = ['#FFD700', '#ffffff']
|
||||
function frame() {
|
||||
confetti({
|
||||
particleCount: 2,
|
||||
angle: 60,
|
||||
spread: 55,
|
||||
origin: { x: 0 },
|
||||
origin: {x: 0},
|
||||
colors: colors,
|
||||
zIndex: 999999
|
||||
})
|
||||
|
|
@ -393,15 +477,14 @@
|
|||
particleCount: 2,
|
||||
angle: 120,
|
||||
spread: 55,
|
||||
origin: { x: 1 },
|
||||
origin: {x: 1},
|
||||
colors: colors,
|
||||
zIndex: 999999
|
||||
})
|
||||
if (Date.now() < end) {
|
||||
requestAnimationFrame(frame)
|
||||
}
|
||||
else {
|
||||
document.getElementById("vue").disabled = false
|
||||
} else {
|
||||
document.getElementById('vue').disabled = false
|
||||
}
|
||||
}
|
||||
frame()
|
||||
|
|
|
|||
|
|
@ -10,15 +10,20 @@
|
|||
<div class="text-center">
|
||||
<a class="text-secondary" href="lightning:{{ lnurl }}">
|
||||
<q-responsive :ratio="1" class="q-mx-md">
|
||||
<qrcode :value="qrValue" :options="{width: 800}" class="rounded-borders"></qrcode>
|
||||
<qrcode
|
||||
:value="qrValue"
|
||||
:options="{width: 800}"
|
||||
class="rounded-borders"
|
||||
></qrcode>
|
||||
</q-responsive>
|
||||
</a>
|
||||
</div>
|
||||
<div class="row q-mt-lg q-gutter-sm">
|
||||
<q-btn outline color="grey" @click="copyText(qrValue)">Copy LNURL </q-btn>
|
||||
<q-btn outline color="grey" @click="copyText(qrValue)"
|
||||
>Copy LNURL
|
||||
</q-btn>
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
</q-card>
|
||||
</div>
|
||||
<div class="col-12 col-sm-6 col-md-5 col-lg-4 q-gutter-y-md">
|
||||
|
|
@ -26,13 +31,15 @@
|
|||
<q-card-section>
|
||||
<h6 class="text-subtitle1 q-mb-sm q-mt-none">Public page</h6>
|
||||
<p class="q-my-none">
|
||||
Most extensions have a public page that can be shared
|
||||
(this page will still be accessible even if you have restricted
|
||||
access to your LNbits install).
|
||||
Most extensions have a public page that can be shared (this page will
|
||||
still be accessible even if you have restricted access to your LNbits
|
||||
install).
|
||||
<br /><br />
|
||||
In this example when a user pays the LNURLpay it triggers an event via a websocket waiting for the payment,
|
||||
which you can subscribe to somewhere using wss://{your-lnbits}/api/v1/ws/{the-id-of-this-record}
|
||||
</q-card-section>
|
||||
In this example when a user pays the LNURLpay it triggers an event via
|
||||
a websocket waiting for the payment, which you can subscribe to
|
||||
somewhere using wss://{your-lnbits}/api/v1/ws/{the-id-of-this-record}
|
||||
</p></q-card-section
|
||||
>
|
||||
<q-card-section class="q-pa-none">
|
||||
<q-separator></q-separator>
|
||||
<q-list> </q-list>
|
||||
|
|
@ -57,8 +64,7 @@
|
|||
// Will trigger payment reaction when payment received, sent from tasks.py
|
||||
eventReactionWebocket(this.myExtensionID)
|
||||
},
|
||||
methods: {
|
||||
}
|
||||
methods: {}
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
11
tests/test_init.py
Normal file
11
tests/test_init.py
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import pytest
|
||||
from fastapi import APIRouter
|
||||
|
||||
from .. import myextension_ext
|
||||
|
||||
|
||||
# just import router and add it to a test router
|
||||
@pytest.mark.asyncio
|
||||
async def test_router():
|
||||
router = APIRouter()
|
||||
router.include_router(myextension_ext)
|
||||
7
toc.md
7
toc.md
|
|
@ -1,22 +1,29 @@
|
|||
# Terms and Conditions for LNbits Extension
|
||||
|
||||
## 1. Acceptance of Terms
|
||||
|
||||
By installing and using the LNbits extension ("Extension"), you agree to be bound by these terms and conditions ("Terms"). If you do not agree to these Terms, do not use the Extension.
|
||||
|
||||
## 2. License
|
||||
|
||||
The Extension is free and open-source software, released under [specify the FOSS license here, e.g., GPL-3.0, MIT, etc.]. You are permitted to use, copy, modify, and distribute the Extension under the terms of that license.
|
||||
|
||||
## 3. No Warranty
|
||||
|
||||
The Extension is provided "as is" and with all faults, and the developer expressly disclaims all warranties of any kind, whether express, implied, statutory, or otherwise, including but not limited to warranties of merchantability, fitness for a particular purpose, non-infringement, and any warranties arising out of course of dealing or usage of trade. No advice or information, whether oral or written, obtained from the developer or elsewhere will create any warranty not expressly stated in this Terms.
|
||||
|
||||
## 4. Limitation of Liability
|
||||
|
||||
In no event will the developer be liable to you or any third party for any direct, indirect, incidental, special, consequential, or punitive damages, including lost profit, lost revenue, loss of data, or other damages arising out of or in connection with your use of the Extension, even if the developer has been advised of the possibility of such damages. The foregoing limitation of liability shall apply to the fullest extent permitted by law in the applicable jurisdiction.
|
||||
|
||||
## 5. Modification of Terms
|
||||
|
||||
The developer reserves the right to modify these Terms at any time. You are advised to review these Terms periodically for any changes. Changes to these Terms are effective when they are posted on the appropriate location within or associated with the Extension.
|
||||
|
||||
## 6. General Provisions
|
||||
|
||||
If any provision of these Terms is held to be invalid or unenforceable, that provision will be enforced to the maximum extent permissible, and the other provisions of these Terms will remain in full force and effect. These Terms constitute the entire agreement between you and the developer regarding the use of the Extension.
|
||||
|
||||
## 7. Contact Information
|
||||
|
||||
If you have any questions about these Terms, please contact the developer at [developer's contact information].
|
||||
|
|
|
|||
34
views.py
34
views.py
|
|
@ -1,18 +1,20 @@
|
|||
from http import HTTPStatus
|
||||
|
||||
from fastapi import Depends, Request
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
from lnbits.core.models import User
|
||||
from lnbits.decorators import check_user_exists
|
||||
from lnbits.helpers import template_renderer
|
||||
from lnbits.settings import settings
|
||||
from starlette.exceptions import HTTPException
|
||||
from starlette.responses import HTMLResponse
|
||||
|
||||
from lnbits.core.models import User
|
||||
from lnbits.decorators import check_user_exists
|
||||
from lnbits.settings import settings
|
||||
|
||||
from . import myextension_ext, myextension_renderer
|
||||
from .crud import get_myextension
|
||||
|
||||
myex = Jinja2Templates(directory="myex")
|
||||
myextension_generic_router = APIRouter()
|
||||
|
||||
|
||||
def myextension_renderer():
|
||||
return template_renderer(["myextension/templates"])
|
||||
|
||||
|
||||
#######################################
|
||||
|
|
@ -23,7 +25,7 @@ myex = Jinja2Templates(directory="myex")
|
|||
# Backend admin page
|
||||
|
||||
|
||||
@myextension_ext.get("/", response_class=HTMLResponse)
|
||||
@myextension_generic_router.get("/", response_class=HTMLResponse)
|
||||
async def index(request: Request, user: User = Depends(check_user_exists)):
|
||||
return myextension_renderer().TemplateResponse(
|
||||
"myextension/index.html", {"request": request, "user": user.dict()}
|
||||
|
|
@ -33,9 +35,9 @@ async def index(request: Request, user: User = Depends(check_user_exists)):
|
|||
# Frontend shareable page
|
||||
|
||||
|
||||
@myextension_ext.get("/{myextension_id}")
|
||||
@myextension_generic_router.get("/{myextension_id}")
|
||||
async def myextension(request: Request, myextension_id):
|
||||
myextension = await get_myextension(myextension_id, request)
|
||||
myextension = await get_myextension(myextension_id)
|
||||
if not myextension:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="MyExtension does not exist."
|
||||
|
|
@ -54,7 +56,7 @@ async def myextension(request: Request, myextension_id):
|
|||
# Manifest for public page, customise or remove manifest completely
|
||||
|
||||
|
||||
@myextension_ext.get("/manifest/{myextension_id}.webmanifest")
|
||||
@myextension_generic_router.get("/manifest/{myextension_id}.webmanifest")
|
||||
async def manifest(myextension_id: str):
|
||||
myextension = await get_myextension(myextension_id)
|
||||
if not myextension:
|
||||
|
|
@ -67,9 +69,11 @@ async def manifest(myextension_id: str):
|
|||
"name": myextension.name + " - " + settings.lnbits_site_title,
|
||||
"icons": [
|
||||
{
|
||||
"src": settings.lnbits_custom_logo
|
||||
if settings.lnbits_custom_logo
|
||||
else "https://cdn.jsdelivr.net/gh/lnbits/lnbits@0.3.0/docs/logos/lnbits.png",
|
||||
"src": (
|
||||
settings.lnbits_custom_logo
|
||||
if settings.lnbits_custom_logo
|
||||
else "https://cdn.jsdelivr.net/gh/lnbits/lnbits@0.3.0/docs/logos/lnbits.png"
|
||||
),
|
||||
"type": "image/png",
|
||||
"sizes": "900x900",
|
||||
}
|
||||
|
|
|
|||
93
views_api.py
93
views_api.py
|
|
@ -1,24 +1,28 @@
|
|||
from http import HTTPStatus
|
||||
from fastapi import Depends, Query, Request
|
||||
from starlette.exceptions import HTTPException
|
||||
|
||||
from fastapi import APIRouter, Depends, Query, Request
|
||||
from lnbits.core.crud import get_user
|
||||
from lnbits.core.models import WalletTypeInfo
|
||||
from lnbits.core.services import create_invoice
|
||||
from lnbits.decorators import (
|
||||
WalletTypeInfo,
|
||||
get_key_type,
|
||||
require_admin_key,
|
||||
require_invoice_key,
|
||||
)
|
||||
from lnbits.helpers import urlsafe_short_hash
|
||||
from lnurl import encode as lnurl_encode
|
||||
from starlette.exceptions import HTTPException
|
||||
|
||||
from . import myextension_ext
|
||||
from .crud import (
|
||||
create_myextension,
|
||||
update_myextension,
|
||||
delete_myextension,
|
||||
get_myextension,
|
||||
get_myextensions,
|
||||
update_myextension,
|
||||
)
|
||||
from .models import CreateMyExtensionData
|
||||
from .models import CreateMyExtensionData, MyExtension
|
||||
|
||||
myextension_api_router = APIRouter()
|
||||
|
||||
|
||||
#######################################
|
||||
|
|
@ -28,9 +32,8 @@ from .models import CreateMyExtensionData
|
|||
## Get all the records belonging to the user
|
||||
|
||||
|
||||
@myextension_ext.get("/api/v1/myex", status_code=HTTPStatus.OK)
|
||||
@myextension_api_router.get("/api/v1/myex", status_code=HTTPStatus.OK)
|
||||
async def api_myextensions(
|
||||
req: Request,
|
||||
all_wallets: bool = Query(False),
|
||||
wallet: WalletTypeInfo = Depends(get_key_type),
|
||||
):
|
||||
|
|
@ -38,19 +41,19 @@ async def api_myextensions(
|
|||
if all_wallets:
|
||||
user = await get_user(wallet.wallet.user)
|
||||
wallet_ids = user.wallet_ids if user else []
|
||||
return [
|
||||
myextension.dict() for myextension in await get_myextensions(wallet_ids, req)
|
||||
]
|
||||
return [myextension.dict() for myextension in await get_myextensions(wallet_ids)]
|
||||
|
||||
|
||||
## Get a single record
|
||||
|
||||
|
||||
@myextension_ext.get("/api/v1/myex/{myextension_id}", status_code=HTTPStatus.OK)
|
||||
async def api_myextension(
|
||||
req: Request, myextension_id: str, WalletTypeInfo=Depends(get_key_type)
|
||||
):
|
||||
myextension = await get_myextension(myextension_id, req)
|
||||
@myextension_api_router.get(
|
||||
"/api/v1/myex/{myextension_id}",
|
||||
status_code=HTTPStatus.OK,
|
||||
dependencies=[Depends(require_invoice_key)],
|
||||
)
|
||||
async def api_myextension(myextension_id: str):
|
||||
myextension = await get_myextension(myextension_id)
|
||||
if not myextension:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="MyExtension does not exist."
|
||||
|
|
@ -61,49 +64,64 @@ async def api_myextension(
|
|||
## update a record
|
||||
|
||||
|
||||
@myextension_ext.put("/api/v1/myex/{myextension_id}")
|
||||
@myextension_api_router.put("/api/v1/myex/{myextension_id}")
|
||||
async def api_myextension_update(
|
||||
req: Request,
|
||||
data: CreateMyExtensionData,
|
||||
myextension_id: str,
|
||||
wallet: WalletTypeInfo = Depends(get_key_type),
|
||||
):
|
||||
) -> MyExtension:
|
||||
if not myextension_id:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="MyExtension does not exist."
|
||||
)
|
||||
myextension = await get_myextension(myextension_id, req)
|
||||
myextension = await get_myextension(myextension_id)
|
||||
assert myextension, "MyExtension couldn't be retrieved"
|
||||
|
||||
if wallet.wallet.id != myextension.wallet:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.FORBIDDEN, detail="Not your MyExtension."
|
||||
)
|
||||
myextension = await update_myextension(
|
||||
myextension_id=myextension_id, **data.dict(), req=req
|
||||
)
|
||||
return myextension.dict()
|
||||
|
||||
for key, value in data.dict().items():
|
||||
setattr(myextension, key, value)
|
||||
|
||||
return await update_myextension(myextension)
|
||||
|
||||
|
||||
## Create a new record
|
||||
|
||||
|
||||
@myextension_ext.post("/api/v1/myex", status_code=HTTPStatus.CREATED)
|
||||
@myextension_api_router.post("/api/v1/myex", status_code=HTTPStatus.CREATED)
|
||||
async def api_myextension_create(
|
||||
req: Request,
|
||||
request: Request,
|
||||
data: CreateMyExtensionData,
|
||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||
):
|
||||
myextension = await create_myextension(
|
||||
wallet_id=wallet.wallet.id, data=data, req=req
|
||||
key_type: WalletTypeInfo = Depends(require_admin_key),
|
||||
) -> MyExtension:
|
||||
myextension_id = urlsafe_short_hash()
|
||||
lnurlpay = lnurl_encode(
|
||||
str(request.url_for("myextension.api_lnurl_pay", myextension_id=myextension_id))
|
||||
)
|
||||
return myextension.dict()
|
||||
lnurlwithdraw = lnurl_encode(
|
||||
str(
|
||||
request.url_for(
|
||||
"myextension.api_lnurl_withdraw", myextension_id=myextension_id
|
||||
)
|
||||
)
|
||||
)
|
||||
data.wallet = data.wallet or key_type.wallet.id
|
||||
myext = MyExtension(
|
||||
id=myextension_id,
|
||||
lnurlpay=lnurlpay,
|
||||
lnurlwithdraw=lnurlwithdraw,
|
||||
**data.dict(),
|
||||
)
|
||||
return await create_myextension(myext)
|
||||
|
||||
|
||||
## Delete a record
|
||||
|
||||
|
||||
@myextension_ext.delete("/api/v1/myex/{myextension_id}")
|
||||
@myextension_api_router.delete("/api/v1/myex/{myextension_id}")
|
||||
async def api_myextension_delete(
|
||||
myextension_id: str, wallet: WalletTypeInfo = Depends(require_admin_key)
|
||||
):
|
||||
|
|
@ -128,7 +146,7 @@ async def api_myextension_delete(
|
|||
## This endpoint creates a payment
|
||||
|
||||
|
||||
@myextension_ext.post(
|
||||
@myextension_api_router.post(
|
||||
"/api/v1/myex/payment/{myextension_id}", status_code=HTTPStatus.CREATED
|
||||
)
|
||||
async def api_myextension_create_invoice(
|
||||
|
|
@ -141,7 +159,8 @@ async def api_myextension_create_invoice(
|
|||
status_code=HTTPStatus.NOT_FOUND, detail="MyExtension does not exist."
|
||||
)
|
||||
|
||||
# we create a payment and add some tags, so tasks.py can grab the payment once its paid
|
||||
# we create a payment and add some tags,
|
||||
# so tasks.py can grab the payment once its paid
|
||||
|
||||
try:
|
||||
payment_hash, payment_request = await create_invoice(
|
||||
|
|
@ -153,7 +172,9 @@ async def api_myextension_create_invoice(
|
|||
"amount": amount,
|
||||
},
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e))
|
||||
except Exception as exc:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(exc)
|
||||
) from exc
|
||||
|
||||
return {"payment_hash": payment_hash, "payment_request": payment_request}
|
||||
|
|
|
|||
|
|
@ -3,15 +3,14 @@
|
|||
# Feel free to delete this file if you don't need it.
|
||||
|
||||
from http import HTTPStatus
|
||||
from fastapi import Depends, Query, Request
|
||||
from . import myextension_ext
|
||||
from .crud import get_myextension
|
||||
from typing import Optional
|
||||
|
||||
import shortuuid
|
||||
from fastapi import APIRouter, Query, Request
|
||||
from lnbits.core.services import create_invoice, pay_invoice
|
||||
from loguru import logger
|
||||
from typing import Optional
|
||||
from .crud import update_myextension
|
||||
from .models import MyExtension
|
||||
import shortuuid
|
||||
|
||||
from .crud import get_myextension
|
||||
|
||||
#################################################
|
||||
########### A very simple LNURLpay ##############
|
||||
|
|
@ -19,8 +18,10 @@ import shortuuid
|
|||
#################################################
|
||||
#################################################
|
||||
|
||||
myextension_lnurl_router = APIRouter()
|
||||
|
||||
@myextension_ext.get(
|
||||
|
||||
@myextension_lnurl_router.get(
|
||||
"/api/v1/lnurl/pay/{myextension_id}",
|
||||
status_code=HTTPStatus.OK,
|
||||
name="myextension.api_lnurl_pay",
|
||||
|
|
@ -45,7 +46,7 @@ async def api_lnurl_pay(
|
|||
}
|
||||
|
||||
|
||||
@myextension_ext.get(
|
||||
@myextension_lnurl_router.get(
|
||||
"/api/v1/lnurl/paycb/{myextension_id}",
|
||||
status_code=HTTPStatus.OK,
|
||||
name="myextension.api_lnurl_pay_callback",
|
||||
|
|
@ -60,7 +61,7 @@ async def api_lnurl_pay_cb(
|
|||
if not myextension:
|
||||
return {"status": "ERROR", "reason": "No myextension found"}
|
||||
|
||||
payment_hash, payment_request = await create_invoice(
|
||||
_, payment_request = await create_invoice(
|
||||
wallet_id=myextension.wallet,
|
||||
amount=int(amount / 1000),
|
||||
memo=myextension.name,
|
||||
|
|
@ -82,28 +83,24 @@ async def api_lnurl_pay_cb(
|
|||
######## A very simple LNURLwithdraw ############
|
||||
# https://github.com/lnurl/luds/blob/luds/03.md #
|
||||
#################################################
|
||||
## withdraws are unique, removing 'tickerhash' ##
|
||||
## here and crud.py will allow muliple pulls ####
|
||||
## withdraw is unlimited, look at withdraw ext ##
|
||||
## for more advanced withdraw options ##
|
||||
#################################################
|
||||
|
||||
|
||||
@myextension_ext.get(
|
||||
"/api/v1/lnurl/withdraw/{myextension_id}/{tickerhash}",
|
||||
@myextension_lnurl_router.get(
|
||||
"/api/v1/lnurl/withdraw/{myextension_id}",
|
||||
status_code=HTTPStatus.OK,
|
||||
name="myextension.api_lnurl_withdraw",
|
||||
)
|
||||
async def api_lnurl_withdraw(
|
||||
request: Request,
|
||||
myextension_id: str,
|
||||
tickerhash: str,
|
||||
):
|
||||
myextension = await get_myextension(myextension_id)
|
||||
if not myextension:
|
||||
return {"status": "ERROR", "reason": "No myextension found"}
|
||||
k1 = shortuuid.uuid(name=myextension.id + str(myextension.ticker))
|
||||
if k1 != tickerhash:
|
||||
return {"status": "ERROR", "reason": "LNURLw already used"}
|
||||
|
||||
k1 = shortuuid.uuid(name=myextension.id)
|
||||
return {
|
||||
"tag": "withdrawRequest",
|
||||
"callback": str(
|
||||
|
|
@ -118,7 +115,7 @@ async def api_lnurl_withdraw(
|
|||
}
|
||||
|
||||
|
||||
@myextension_ext.get(
|
||||
@myextension_lnurl_router.get(
|
||||
"/api/v1/lnurl/withdrawcb/{myextension_id}",
|
||||
status_code=HTTPStatus.OK,
|
||||
name="myextension.api_lnurl_withdraw_callback",
|
||||
|
|
@ -134,13 +131,10 @@ async def api_lnurl_withdraw_cb(
|
|||
if not myextension:
|
||||
return {"status": "ERROR", "reason": "No myextension found"}
|
||||
|
||||
k1Check = shortuuid.uuid(name=myextension.id + str(myextension.ticker))
|
||||
if k1Check != k1:
|
||||
k1_check = shortuuid.uuid(name=myextension.id)
|
||||
if k1_check != k1:
|
||||
return {"status": "ERROR", "reason": "Wrong k1 check provided"}
|
||||
|
||||
await update_myextension(
|
||||
myextension_id=myextension_id, ticker=myextension.ticker + 1
|
||||
)
|
||||
await pay_invoice(
|
||||
wallet_id=myextension.wallet,
|
||||
payment_request=pr,
|
||||
Loading…
Add table
Add a link
Reference in a new issue