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
gh config set pager cat
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
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
> 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)
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`.
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.

View file

@ -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
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.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,))

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:
* Functionality
* Use cases
- Functionality
- Use cases
...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
# 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;
"""
)

View file

@ -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
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
#######################################
########## 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,

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-expansion-item>
</q-expansion-item>

View file

@ -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>
</q-expansion-item>

View file

@ -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()
@ -445,4 +528,4 @@
}
})
</script>
{% endblock %}
{% endblock %}

View file

@ -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 %}
{% 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
## 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].

View file

@ -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",
}

View file

@ -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}

View file

@ -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,