feat: code quality

format

add ci

fixup ci
This commit is contained in:
dni ⚡ 2024-08-12 14:52:42 +02:00
parent 09bb033f85
commit 42b5edaf5d
26 changed files with 3199 additions and 295 deletions

34
.github/workflows/ci.yml vendored Normal file
View 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 }})'

View file

@ -55,4 +55,4 @@ jobs:
# check if pr exists before creating it # check if pr exists before creating it
gh config set pager cat gh config set pager cat
check=$(gh pr list -H $branch | wc -l) check=$(gh pr list -H $branch | wc -l)
test $check -ne 0 || gh pr create --title "$title" --body "$body" --repo lnbits/lnbits-extensions test $check -ne 0 || gh pr create --title "$title" --body "$body" --repo lnbits/lnbits-extensions

5
.gitignore vendored
View file

@ -1 +1,4 @@
__pycache__ __pycache__
node_modules
.mypy_cache
.venv

12
.prettierrc Normal file
View 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
View 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"

View file

@ -4,15 +4,16 @@
## A Starter Template for Your Own Extension ## A Starter Template for Your Own Extension
Ready to start hacking? Once you've forked this extension, you can incorporate functions from other extensions as needed. Ready to start hacking? Once you've forked this extension, you can incorporate functions from other extensions as needed.
### How to Use This Template ### 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. > 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"`. ![Extension Sources](https://i.imgur.com/MUGwAU3.png) ![image](https://github.com/lnbits/myextension/assets/33088785/4133123b-c747-4458-ba6c-5cc7c0f124d8) 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"`. ![Extension Sources](https://i.imgur.com/MUGwAU3.png) ![image](https://github.com/lnbits/myextension/assets/33088785/4133123b-c747-4458-ba6c-5cc7c0f124d8)
2. `Ctrl c` shut down your LNbits installation. 2. `Ctrl c` shut down your LNbits installation.
3. Download the extension files from https://github.com/lnbits/myextension to a folder outside of `/lnbits`, and initialize the folder with `git`. Alternatively, create a repo, copy the myextension extension files into it, then `git clone` the extension to a location outside of `/lnbits`. 3. Download the extension files from https://github.com/lnbits/myextension to a folder outside of `/lnbits`, and initialize the folder with `git`. Alternatively, create a repo, copy the myextension extension files into it, then `git clone` the extension to a location outside of `/lnbits`.
4. Remove the installed extension from `lnbits/lnbits/extensions`. 4. Remove the installed extension from `lnbits/lnbits/extensions`.
5. Create a symbolic link using `ln -s /home/ben/Projects/<name of your extension> /home/ben/Projects/lnbits/lnbits/extensions`. 5. Create a symbolic link using `ln -s /home/ben/Projects/<name of your extension> /home/ben/Projects/lnbits/lnbits/extensions`.
6. Restart your LNbits installation. You can now modify your extension and `git push` changes to a repo. 6. Restart your LNbits installation. You can now modify your extension and `git push` changes to a repo.

View file

@ -1,19 +1,24 @@
import asyncio import asyncio
from fastapi import APIRouter 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 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( 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: 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 = [ 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] = [] scheduled_tasks: list[asyncio.Task] = []
@ -44,5 +39,16 @@ def myextension_stop():
def myextension_start(): def myextension_start():
from lnbits.tasks import create_permanent_unique_task
task = create_permanent_unique_task("ext_myextension", wait_for_paid_invoices) task = create_permanent_unique_task("ext_myextension", wait_for_paid_invoices)
scheduled_tasks.append(task) scheduled_tasks.append(task)
__all__ = [
"db",
"myextension_ext",
"myextension_static_files",
"myextension_start",
"myextension_stop",
]

128
crud.py
View file

@ -1,99 +1,77 @@
from typing import List, Optional, Union from typing import Optional, Union
from lnbits.helpers import urlsafe_short_hash from lnbits.db import Database
from lnbits.lnurl import encode as lnurl_encode from lnbits.helpers import insert_query, update_query
from . import db
from .models import CreateMyExtensionData, MyExtension from .models import MyExtension
from fastapi import Request
from lnurl import encode as lnurl_encode db = Database("ext_myextension")
import shortuuid table_name = "myextension.maintable"
async def create_myextension( async def create_myextension(data: MyExtension) -> MyExtension:
wallet_id: str, data: CreateMyExtensionData, req: Request
) -> MyExtension:
myextension_id = urlsafe_short_hash()
await db.execute( await db.execute(
""" insert_query(table_name, data),
INSERT INTO myextension.maintable (id, wallet, name, lnurlpayamount, lnurlwithdrawamount) (*data.dict().values(),),
VALUES (?, ?, ?, ?, ?)
""",
(
myextension_id,
wallet_id,
data.name,
data.lnurlpayamount,
data.lnurlwithdrawamount,
),
) )
myextension = await get_myextension(myextension_id, req) return data
assert myextension, "Newly created table couldn't be retrieved"
return myextension # 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( async def get_myextension(myextension_id: str) -> Optional[MyExtension]:
myextension_id: str, req: Optional[Request] = None
) -> Optional[MyExtension]:
row = await db.fetchone( 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 MyExtension(**row) if row else None
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
async def get_myextensions( async def get_myextensions(wallet_ids: Union[str, list[str]]) -> list[MyExtension]:
wallet_ids: Union[str, List[str]], req: Optional[Request] = None
) -> List[MyExtension]:
if isinstance(wallet_ids, str): if isinstance(wallet_ids, str):
wallet_ids = [wallet_ids] wallet_ids = [wallet_ids]
q = ",".join(["?"] * len(wallet_ids)) q = ",".join(["?"] * len(wallet_ids))
rows = await db.fetchall( 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] return [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
async def update_myextension( async def update_myextension(data: MyExtension) -> MyExtension:
myextension_id: str, req: Optional[Request] = None, **kwargs
) -> MyExtension:
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
await db.execute( await db.execute(
f"UPDATE myextension.maintable SET {q} WHERE id = ?", update_query(table_name, data),
(*kwargs.values(), myextension_id), (
*data.dict().values(),
data.id,
),
) )
myextension = await get_myextension(myextension_id, req) return data
assert myextension, "Newly updated myextension couldn't be retrieved" # this is how we used to do it
return myextension
# 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: async def delete_myextension(myextension_id: str) -> None:
await db.execute( await db.execute(f"DELETE FROM {table_name} WHERE id = ?", (myextension_id,))
"DELETE FROM myextension.maintable WHERE id = ?", (myextension_id,)
)

View file

@ -4,7 +4,7 @@ This is a longform description that will be used in the advanced description whe
Adding some bullets is nice covering: Adding some bullets is nice covering:
* Functionality - Functionality
* Use cases - Use cases
...and some other text about just how great this etension is. ...and some other text about just how great this etension is.

View file

@ -1,5 +1,6 @@
# the migration file is where you build your database tables # 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): 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;
"""
)

View file

@ -1,30 +1,24 @@
# Data models for your extension # Data models for your extension
from sqlite3 import Row
from typing import Optional from typing import Optional
from pydantic import BaseModel from pydantic import BaseModel
class CreateMyExtensionData(BaseModel): class CreateMyExtensionData(BaseModel):
wallet: Optional[str] name: str
name: Optional[str] lnurlpayamount: int
total: Optional[int] lnurlwithdrawamount: int
lnurlpayamount: Optional[int] wallet: Optional[str] = None
lnurlwithdrawamount: Optional[int] total: int = 0
ticker: Optional[int]
class MyExtension(BaseModel): class MyExtension(BaseModel):
id: str id: str
wallet: Optional[str] wallet: str
name: Optional[str] lnurlpayamount: int
total: Optional[int] name: str
lnurlpayamount: Optional[int] lnurlwithdrawamount: int
lnurlwithdrawamount: Optional[int] total: int
lnurlpay: Optional[str] lnurlpay: Optional[str]
lnurlwithdraw: 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
View 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
View 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

File diff suppressed because it is too large Load diff

89
pyproject.toml Normal file
View 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",
]

View file

@ -7,7 +7,6 @@ from lnbits.tasks import register_invoice_listener
from .crud import get_myextension, update_myextension from .crud import get_myextension, update_myextension
####################################### #######################################
########## RUN YOUR TASKS HERE ######## ########## RUN YOUR TASKS HERE ########
####################################### #######################################
@ -31,19 +30,22 @@ async def on_invoice_paid(payment: Payment) -> None:
return return
myextension_id = payment.extra.get("myextensionId") myextension_id = payment.extra.get("myextensionId")
assert myextension_id, "myextensionId not set in invoice"
myextension = await get_myextension(myextension_id) myextension = await get_myextension(myextension_id)
assert myextension, "MyExtension does not exist"
# update something in the db # update something in the db
if payment.extra.get("lnurlwithdraw"): if payment.extra.get("lnurlwithdraw"):
total = myextension.total - payment.amount total = myextension.total - payment.amount
else: else:
total = myextension.total + payment.amount 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> # here we could send some data to a websocket on
# and then listen to it on the frontend, which we do with index.html connectWebocket() # 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 = { some_payment_data = {
"name": myextension.name, "name": myextension.name,

View file

@ -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-btn flat label="Swagger API" type="a" href="../docs#/MyExtension"></q-btn>
</q-expansion-item> </q-expansion-item>

View file

@ -1,13 +1,25 @@
<q-expansion-item group="extras" icon="info" label="More info"> <q-expansion-item group="extras" icon="info" label="More info">
<q-card> <q-card>
<q-card-section> <q-card-section>
<p> <p>Some more info about my excellent extension.</p>
Some more info about my excellent extension. <small
</p> >Created by
<small>Created by <a
<a class="text-secondary" href="https://github.com/benarc" target="_blank">Ben Arc</a>.</small> class="text-secondary"
<small>Repo href="https://github.com/benarc"
<a class="text-secondary" href="https://github.com/lnbits/myextension" target="_blank">MyExtension</a>.</small> 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-section>
</q-card> </q-card>
</q-expansion-item> </q-expansion-item>

View file

@ -8,7 +8,9 @@
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md"> <div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
<q-card> <q-card>
<q-card-section> <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-section>
</q-card> </q-card>
@ -22,8 +24,14 @@
<q-btn flat color="grey" @click="exportCSV">Export to CSV</q-btn> <q-btn flat color="grey" @click="exportCSV">Export to CSV</q-btn>
</div> </div>
</div> </div>
<q-table dense flat :data="myex" row-key="id" :columns="myexTable.columns" <q-table
:pagination.sync="myexTable.pagination"> dense
flat
:data="myex"
row-key="id"
:columns="myexTable.columns"
:pagination.sync="myexTable.pagination"
>
<myextension v-slot:header="props"> <myextension v-slot:header="props">
<q-tr :props="props"> <q-tr :props="props">
<q-th v-for="col in props.cols" :key="col.name" :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> <div v-else>${ col.value }</div>
</q-td> </q-td>
<q-td auto-width> <q-td auto-width>
<q-btn unelevated dense size="sm" icon="qr_code" :color="($q.dark.isActive) ? 'grey-7' : 'grey-5'" <q-btn
class="q-mr-sm" @click="openUrlDialog(props.row.id)"></q-btn></q-td> 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-td auto-width>
<q-btn unelevated dense size="sm" icon="launch" :color="($q.dark.isActive) ? 'grey-7' : 'grey-5'" <q-btn
type="a" :href="props.row.myextension" target="_blank"><q-tooltip>Open public unelevated
page</q-tooltip></q-btn></q-td> 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-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-tooltip> Edit copilot </q-tooltip>
</q-btn> </q-btn>
</q-td> </q-td>
<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-tooltip> Delete copilot </q-tooltip>
</q-btn> </q-btn>
</q-td> </q-td>
</q-tr> </q-tr>
</template> </template>
</q-table> </q-table>
</q-card-section> </q-card-section>
</q-card> </q-card>
@ -69,9 +106,13 @@
<div class="col-12 col-md-4 col-lg-5 q-gutter-y-md"> <div class="col-12 col-md-4 col-lg-5 q-gutter-y-md">
<q-card> <q-card>
<q-card-section> <q-card-section>
<h6 class="text-subtitle1 q-my-none">{{SITE_TITLE}} MyExtension extension</h6> <h6 class="text-subtitle1 q-my-none">
<p>Simple extension you can use as a base for your own extension. <br /> Includes very simple LNURL-pay and {{SITE_TITLE}} MyExtension extension
LNURL-withdraw example.</p> </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>
<q-card-section class="q-pa-none"> <q-card-section class="q-pa-none">
<q-separator></q-separator> <q-separator></q-separator>
@ -91,20 +132,54 @@
<q-dialog v-model="formDialog.show" position="top" @hide="closeFormDialog"> <q-dialog v-model="formDialog.show" position="top" @hide="closeFormDialog">
<q-card class="q-pa-lg q-pt-xl" style="width: 500px"> <q-card class="q-pa-lg q-pt-xl" style="width: 500px">
<q-form @submit="sendMyExtensionData" class="q-gutter-md"> <q-form @submit="sendMyExtensionData" class="q-gutter-md">
<q-input filled dense v-model.trim="formDialog.data.name" label="Name" <q-input
placeholder="Name for your record"></q-input> filled
<q-select filled dense emit-value v-model="formDialog.data.wallet" :options="g.user.walletOptions" dense
label="Wallet *"></q-select> v-model.trim="formDialog.data.name"
<q-input filled dense type="number" v-model.trim="formDialog.data.lnurlwithdrawamount" label="Name"
label="LNURL-withdraw amount"></q-input> placeholder="Name for your record"
<q-input filled dense type="number" v-model.trim="formDialog.data.lnurlpayamount" ></q-input>
label="LNURL-pay amount"></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"> <div class="row q-mt-lg">
<q-btn v-if="formDialog.data.id" unelevated color="primary" type="submit">Update MyExtension</q-btn> <q-btn
<q-btn v-else unelevated color="primary" 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" :disable="formDialog.data.name == null || formDialog.data.wallet == null || formDialog.data.lnurlwithdrawamount == null || formDialog.data.lnurlpayamount == null"
type="submit">Create MyExtension</q-btn> type="submit"
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn> >Create MyExtension</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div> </div>
</q-form> </q-form>
</q-card> </q-card>
@ -119,29 +194,39 @@
<q-responsive :ratio="1" class="q-mx-xl q-mb-md"> <q-responsive :ratio="1" class="q-mx-xl q-mb-md">
<lnbits-qrcode :value="qrValue"></lnbits-qrcode> <lnbits-qrcode :value="qrValue"></lnbits-qrcode>
</q-responsive> </q-responsive>
<center><q-btn label="copy" @click="copyText(qrValue)"></q-btn> <center><q-btn label="copy" @click="copyText(qrValue)"></q-btn></center>
</center>
<q-separator></q-separator> <q-separator></q-separator>
<div class="row justify-start q-mt-lg"> <div class="row justify-start q-mt-lg">
<div class="col col-md-auto"> <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>
<div class="col col-md-auto"> <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>
<div class="col q-pl-md"> <div class="col q-pl-md">
<q-input filled bottom-slots dense v-model="invoiceAmount"> <q-input filled bottom-slots dense v-model="invoiceAmount">
<template v-slot:append> <template v-slot:append>
<q-btn round @click="createInvoice(urlDialog.data.wallet, urlDialog.data.id)" color="primary" flat <q-btn
icon="add_circle" /> round
</template> @click="createInvoice(urlDialog.data.wallet, urlDialog.data.id)"
<template v-slot:hint> color="primary"
Create an invoice flat
icon="add_circle"
/>
</template> </template>
<template v-slot:hint> Create an invoice </template>
</q-input> </q-input>
</div> </div>
</div> </div>
@ -150,12 +235,10 @@
</div> </div>
</q-card> </q-card>
</q-dialog> </q-dialog>
</div> </div>
{% endblock %} {% block scripts %} {{ window_vars(user) }} {% 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 src="https://cdn.jsdelivr.net/npm/canvas-confetti@1.4.0/dist/confetti.browser.min.js"></script>
<script> <script>
/////////////////////////////////////////////////// ///////////////////////////////////////////////////
//////////an object we can update with data//////// //////////an object we can update with data////////
/////////////////////////////////////////////////// ///////////////////////////////////////////////////
@ -178,15 +261,20 @@
myex: [], myex: [],
myexTable: { myexTable: {
columns: [ columns: [
{ name: 'id', align: 'left', label: 'ID', field: 'id' }, {name: 'id', align: 'left', label: 'ID', field: 'id'},
{ name: 'name', align: 'left', label: 'Name', field: 'name' }, {name: 'name', align: 'left', label: 'Name', field: 'name'},
{ {
name: 'wallet', name: 'wallet',
align: 'left', align: 'left',
label: 'Wallet', label: 'Wallet',
field: 'wallet' field: 'wallet'
}, },
{ name: 'total', align: 'left', label: 'Total sent/received', field: 'total' }, {
name: 'total',
align: 'left',
label: 'Total sent/received',
field: 'total'
}
], ],
pagination: { pagination: {
rowsPerPage: 10 rowsPerPage: 10
@ -247,7 +335,7 @@
} }
}, },
updateMyExtensionForm(tempId) { updateMyExtensionForm(tempId) {
const myextension = _.findWhere(this.myex, { id: tempId }) const myextension = _.findWhere(this.myex, {id: tempId})
this.formDialog.data = { this.formDialog.data = {
...myextension ...myextension
} }
@ -291,7 +379,7 @@
}, },
deleteMyExtension: function (tempId) { deleteMyExtension: function (tempId) {
var self = this var self = this
var myextension = _.findWhere(this.myex, { id: tempId }) var myextension = _.findWhere(this.myex, {id: tempId})
LNbits.utils LNbits.utils
.confirmDialog('Are you sure you want to delete this MyExtension?') .confirmDialog('Are you sure you want to delete this MyExtension?')
@ -300,7 +388,8 @@
.request( .request(
'DELETE', 'DELETE',
'/myextension/api/v1/myex/' + tempId, '/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) { .then(function (response) {
self.myex = _.reject(self.myex, function (obj) { self.myex = _.reject(self.myex, function (obj) {
@ -316,12 +405,12 @@
LNbits.utils.exportCSV(this.myexTable.columns, this.myex) LNbits.utils.exportCSV(this.myexTable.columns, this.myex)
}, },
itemsArray(tempId) { itemsArray(tempId) {
const myextension = _.findWhere(this.myex, { id: tempId }) const myextension = _.findWhere(this.myex, {id: tempId})
return [...myextension.itemsMap.values()] return [...myextension.itemsMap.values()]
}, },
openformDialog(id) { openformDialog(id) {
const [tempId, itemId] = id.split(':') const [tempId, itemId] = id.split(':')
const myextension = _.findWhere(this.myex, { id: tempId }) const myextension = _.findWhere(this.myex, {id: tempId})
if (itemId) { if (itemId) {
const item = myextension.itemsMap.get(id) const item = myextension.itemsMap.get(id)
this.formDialog.data = { this.formDialog.data = {
@ -339,7 +428,7 @@
this.formDialog.data = {} this.formDialog.data = {}
}, },
openUrlDialog(id) { openUrlDialog(id) {
this.urlDialog.data = _.findWhere(this.myex, { id }) this.urlDialog.data = _.findWhere(this.myex, {id})
this.qrValue = this.urlDialog.data.lnurlpay this.qrValue = this.urlDialog.data.lnurlpay
console.log(this.urlDialog.data.id) console.log(this.urlDialog.data.id)
this.connectWebocket(this.urlDialog.data.id) this.connectWebocket(this.urlDialog.data.id)
@ -363,12 +452,7 @@
} }
} }
LNbits.api LNbits.api
.request( .request('POST', `/api/v1/payments`, wallet.inkey, dataToSend)
'POST',
`/api/v1/payments`,
wallet.inkey,
dataToSend
)
.then(response => { .then(response => {
this.qrValue = response.data.payment_request this.qrValue = response.data.payment_request
}) })
@ -377,15 +461,15 @@
}) })
}, },
makeItRain() { makeItRain() {
document.getElementById("vue").disabled = true document.getElementById('vue').disabled = true
var end = Date.now() + (2 * 1000) var end = Date.now() + 2 * 1000
var colors = ['#FFD700', '#ffffff'] var colors = ['#FFD700', '#ffffff']
function frame() { function frame() {
confetti({ confetti({
particleCount: 2, particleCount: 2,
angle: 60, angle: 60,
spread: 55, spread: 55,
origin: { x: 0 }, origin: {x: 0},
colors: colors, colors: colors,
zIndex: 999999 zIndex: 999999
}) })
@ -393,15 +477,14 @@
particleCount: 2, particleCount: 2,
angle: 120, angle: 120,
spread: 55, spread: 55,
origin: { x: 1 }, origin: {x: 1},
colors: colors, colors: colors,
zIndex: 999999 zIndex: 999999
}) })
if (Date.now() < end) { if (Date.now() < end) {
requestAnimationFrame(frame) requestAnimationFrame(frame)
} } else {
else { document.getElementById('vue').disabled = false
document.getElementById("vue").disabled = false
} }
} }
frame() frame()
@ -445,4 +528,4 @@
} }
}) })
</script> </script>
{% endblock %} {% endblock %}

View file

@ -10,15 +10,20 @@
<div class="text-center"> <div class="text-center">
<a class="text-secondary" href="lightning:{{ lnurl }}"> <a class="text-secondary" href="lightning:{{ lnurl }}">
<q-responsive :ratio="1" class="q-mx-md"> <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> </q-responsive>
</a> </a>
</div> </div>
<div class="row q-mt-lg q-gutter-sm"> <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> </div>
</q-card-section> </q-card-section>
</q-card> </q-card>
</div> </div>
<div class="col-12 col-sm-6 col-md-5 col-lg-4 q-gutter-y-md"> <div class="col-12 col-sm-6 col-md-5 col-lg-4 q-gutter-y-md">
@ -26,13 +31,15 @@
<q-card-section> <q-card-section>
<h6 class="text-subtitle1 q-mb-sm q-mt-none">Public page</h6> <h6 class="text-subtitle1 q-mb-sm q-mt-none">Public page</h6>
<p class="q-my-none"> <p class="q-my-none">
Most extensions have a public page that can be shared Most extensions have a public page that can be shared (this page will
(this page will still be accessible even if you have restricted still be accessible even if you have restricted access to your LNbits
access to your LNbits install). install).
<br /><br /> <br /><br />
In this example when a user pays the LNURLpay it triggers an event via a websocket waiting for the payment, In this example when a user pays the LNURLpay it triggers an event via
which you can subscribe to somewhere using wss://{your-lnbits}/api/v1/ws/{the-id-of-this-record} a websocket waiting for the payment, which you can subscribe to
</q-card-section> 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-card-section class="q-pa-none">
<q-separator></q-separator> <q-separator></q-separator>
<q-list> </q-list> <q-list> </q-list>
@ -57,8 +64,7 @@
// Will trigger payment reaction when payment received, sent from tasks.py // Will trigger payment reaction when payment received, sent from tasks.py
eventReactionWebocket(this.myExtensionID) eventReactionWebocket(this.myExtensionID)
}, },
methods: { methods: {}
}
}) })
</script> </script>
{% endblock %} {% endblock %}

0
tests/__init__.py Normal file
View file

11
tests/test_init.py Normal file
View 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
View file

@ -1,22 +1,29 @@
# Terms and Conditions for LNbits Extension # Terms and Conditions for LNbits Extension
## 1. Acceptance of Terms ## 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. 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 ## 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. 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 ## 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. 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 ## 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. 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 ## 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. 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 ## 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. 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 ## 7. Contact Information
If you have any questions about these Terms, please contact the developer at [developer's contact information]. If you have any questions about these Terms, please contact the developer at [developer's contact information].

View file

@ -1,18 +1,20 @@
from http import HTTPStatus from http import HTTPStatus
from fastapi import Depends, Request from fastapi import APIRouter, Depends, Request
from fastapi.templating import Jinja2Templates 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.exceptions import HTTPException
from starlette.responses import HTMLResponse 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 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 # 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)): async def index(request: Request, user: User = Depends(check_user_exists)):
return myextension_renderer().TemplateResponse( return myextension_renderer().TemplateResponse(
"myextension/index.html", {"request": request, "user": user.dict()} "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 # Frontend shareable page
@myextension_ext.get("/{myextension_id}") @myextension_generic_router.get("/{myextension_id}")
async def myextension(request: Request, 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: if not myextension:
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="MyExtension does not exist." 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 # 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): async def manifest(myextension_id: str):
myextension = await get_myextension(myextension_id) myextension = await get_myextension(myextension_id)
if not myextension: if not myextension:
@ -67,9 +69,11 @@ async def manifest(myextension_id: str):
"name": myextension.name + " - " + settings.lnbits_site_title, "name": myextension.name + " - " + settings.lnbits_site_title,
"icons": [ "icons": [
{ {
"src": settings.lnbits_custom_logo "src": (
if settings.lnbits_custom_logo settings.lnbits_custom_logo
else "https://cdn.jsdelivr.net/gh/lnbits/lnbits@0.3.0/docs/logos/lnbits.png", if settings.lnbits_custom_logo
else "https://cdn.jsdelivr.net/gh/lnbits/lnbits@0.3.0/docs/logos/lnbits.png"
),
"type": "image/png", "type": "image/png",
"sizes": "900x900", "sizes": "900x900",
} }

View file

@ -1,24 +1,28 @@
from http import HTTPStatus 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.crud import get_user
from lnbits.core.models import WalletTypeInfo
from lnbits.core.services import create_invoice from lnbits.core.services import create_invoice
from lnbits.decorators import ( from lnbits.decorators import (
WalletTypeInfo,
get_key_type, get_key_type,
require_admin_key, 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 ( from .crud import (
create_myextension, create_myextension,
update_myextension,
delete_myextension, delete_myextension,
get_myextension, get_myextension,
get_myextensions, 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 ## 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( async def api_myextensions(
req: Request,
all_wallets: bool = Query(False), all_wallets: bool = Query(False),
wallet: WalletTypeInfo = Depends(get_key_type), wallet: WalletTypeInfo = Depends(get_key_type),
): ):
@ -38,19 +41,19 @@ async def api_myextensions(
if all_wallets: if all_wallets:
user = await get_user(wallet.wallet.user) user = await get_user(wallet.wallet.user)
wallet_ids = user.wallet_ids if user else [] wallet_ids = user.wallet_ids if user else []
return [ return [myextension.dict() for myextension in await get_myextensions(wallet_ids)]
myextension.dict() for myextension in await get_myextensions(wallet_ids, req)
]
## Get a single record ## Get a single record
@myextension_ext.get("/api/v1/myex/{myextension_id}", status_code=HTTPStatus.OK) @myextension_api_router.get(
async def api_myextension( "/api/v1/myex/{myextension_id}",
req: Request, myextension_id: str, WalletTypeInfo=Depends(get_key_type) status_code=HTTPStatus.OK,
): dependencies=[Depends(require_invoice_key)],
myextension = await get_myextension(myextension_id, req) )
async def api_myextension(myextension_id: str):
myextension = await get_myextension(myextension_id)
if not myextension: if not myextension:
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="MyExtension does not exist." status_code=HTTPStatus.NOT_FOUND, detail="MyExtension does not exist."
@ -61,49 +64,64 @@ async def api_myextension(
## update a record ## 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( async def api_myextension_update(
req: Request,
data: CreateMyExtensionData, data: CreateMyExtensionData,
myextension_id: str, myextension_id: str,
wallet: WalletTypeInfo = Depends(get_key_type), wallet: WalletTypeInfo = Depends(get_key_type),
): ) -> MyExtension:
if not myextension_id: if not myextension_id:
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="MyExtension does not exist." 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" assert myextension, "MyExtension couldn't be retrieved"
if wallet.wallet.id != myextension.wallet: if wallet.wallet.id != myextension.wallet:
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="Not your MyExtension." status_code=HTTPStatus.FORBIDDEN, detail="Not your MyExtension."
) )
myextension = await update_myextension(
myextension_id=myextension_id, **data.dict(), req=req for key, value in data.dict().items():
) setattr(myextension, key, value)
return myextension.dict()
return await update_myextension(myextension)
## Create a new record ## 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( async def api_myextension_create(
req: Request, request: Request,
data: CreateMyExtensionData, data: CreateMyExtensionData,
wallet: WalletTypeInfo = Depends(require_admin_key), key_type: WalletTypeInfo = Depends(require_admin_key),
): ) -> MyExtension:
myextension = await create_myextension( myextension_id = urlsafe_short_hash()
wallet_id=wallet.wallet.id, data=data, req=req 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 ## 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( async def api_myextension_delete(
myextension_id: str, wallet: WalletTypeInfo = Depends(require_admin_key) myextension_id: str, wallet: WalletTypeInfo = Depends(require_admin_key)
): ):
@ -128,7 +146,7 @@ async def api_myextension_delete(
## This endpoint creates a payment ## This endpoint creates a payment
@myextension_ext.post( @myextension_api_router.post(
"/api/v1/myex/payment/{myextension_id}", status_code=HTTPStatus.CREATED "/api/v1/myex/payment/{myextension_id}", status_code=HTTPStatus.CREATED
) )
async def api_myextension_create_invoice( 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." 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: try:
payment_hash, payment_request = await create_invoice( payment_hash, payment_request = await create_invoice(
@ -153,7 +172,9 @@ async def api_myextension_create_invoice(
"amount": amount, "amount": amount,
}, },
) )
except Exception as e: except Exception as exc:
raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e)) raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(exc)
) from exc
return {"payment_hash": payment_hash, "payment_request": payment_request} return {"payment_hash": payment_hash, "payment_request": payment_request}

View file

@ -3,15 +3,14 @@
# Feel free to delete this file if you don't need it. # Feel free to delete this file if you don't need it.
from http import HTTPStatus from http import HTTPStatus
from fastapi import Depends, Query, Request from typing import Optional
from . import myextension_ext
from .crud import get_myextension import shortuuid
from fastapi import APIRouter, Query, Request
from lnbits.core.services import create_invoice, pay_invoice from lnbits.core.services import create_invoice, pay_invoice
from loguru import logger from loguru import logger
from typing import Optional
from .crud import update_myextension from .crud import get_myextension
from .models import MyExtension
import shortuuid
################################################# #################################################
########### A very simple LNURLpay ############## ########### 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}", "/api/v1/lnurl/pay/{myextension_id}",
status_code=HTTPStatus.OK, status_code=HTTPStatus.OK,
name="myextension.api_lnurl_pay", 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}", "/api/v1/lnurl/paycb/{myextension_id}",
status_code=HTTPStatus.OK, status_code=HTTPStatus.OK,
name="myextension.api_lnurl_pay_callback", name="myextension.api_lnurl_pay_callback",
@ -60,7 +61,7 @@ async def api_lnurl_pay_cb(
if not myextension: if not myextension:
return {"status": "ERROR", "reason": "No myextension found"} return {"status": "ERROR", "reason": "No myextension found"}
payment_hash, payment_request = await create_invoice( _, payment_request = await create_invoice(
wallet_id=myextension.wallet, wallet_id=myextension.wallet,
amount=int(amount / 1000), amount=int(amount / 1000),
memo=myextension.name, memo=myextension.name,
@ -82,28 +83,24 @@ async def api_lnurl_pay_cb(
######## A very simple LNURLwithdraw ############ ######## A very simple LNURLwithdraw ############
# https://github.com/lnurl/luds/blob/luds/03.md # # https://github.com/lnurl/luds/blob/luds/03.md #
################################################# #################################################
## withdraws are unique, removing 'tickerhash' ## ## withdraw is unlimited, look at withdraw ext ##
## here and crud.py will allow muliple pulls #### ## for more advanced withdraw options ##
################################################# #################################################
@myextension_ext.get( @myextension_lnurl_router.get(
"/api/v1/lnurl/withdraw/{myextension_id}/{tickerhash}", "/api/v1/lnurl/withdraw/{myextension_id}",
status_code=HTTPStatus.OK, status_code=HTTPStatus.OK,
name="myextension.api_lnurl_withdraw", name="myextension.api_lnurl_withdraw",
) )
async def api_lnurl_withdraw( async def api_lnurl_withdraw(
request: Request, request: Request,
myextension_id: str, myextension_id: str,
tickerhash: str,
): ):
myextension = await get_myextension(myextension_id) myextension = await get_myextension(myextension_id)
if not myextension: if not myextension:
return {"status": "ERROR", "reason": "No myextension found"} return {"status": "ERROR", "reason": "No myextension found"}
k1 = shortuuid.uuid(name=myextension.id + str(myextension.ticker)) k1 = shortuuid.uuid(name=myextension.id)
if k1 != tickerhash:
return {"status": "ERROR", "reason": "LNURLw already used"}
return { return {
"tag": "withdrawRequest", "tag": "withdrawRequest",
"callback": str( "callback": str(
@ -118,7 +115,7 @@ async def api_lnurl_withdraw(
} }
@myextension_ext.get( @myextension_lnurl_router.get(
"/api/v1/lnurl/withdrawcb/{myextension_id}", "/api/v1/lnurl/withdrawcb/{myextension_id}",
status_code=HTTPStatus.OK, status_code=HTTPStatus.OK,
name="myextension.api_lnurl_withdraw_callback", name="myextension.api_lnurl_withdraw_callback",
@ -134,13 +131,10 @@ async def api_lnurl_withdraw_cb(
if not myextension: if not myextension:
return {"status": "ERROR", "reason": "No myextension found"} return {"status": "ERROR", "reason": "No myextension found"}
k1Check = shortuuid.uuid(name=myextension.id + str(myextension.ticker)) k1_check = shortuuid.uuid(name=myextension.id)
if k1Check != k1: if k1_check != k1:
return {"status": "ERROR", "reason": "Wrong k1 check provided"} return {"status": "ERROR", "reason": "Wrong k1 check provided"}
await update_myextension(
myextension_id=myextension_id, ticker=myextension.ticker + 1
)
await pay_invoice( await pay_invoice(
wallet_id=myextension.wallet, wallet_id=myextension.wallet,
payment_request=pr, payment_request=pr,