overwrite mock data
This commit is contained in:
commit
7bfd8f3a62
105 changed files with 1550 additions and 734 deletions
10
.env.example
10
.env.example
|
|
@ -1,16 +1,22 @@
|
||||||
HOST=127.0.0.1
|
HOST=127.0.0.1
|
||||||
PORT=5000
|
PORT=5000
|
||||||
|
|
||||||
|
# uvicorn variable, allow https behind a proxy
|
||||||
|
# FORWARDED_ALLOW_IPS="*"
|
||||||
|
|
||||||
DEBUG=false
|
DEBUG=false
|
||||||
|
|
||||||
|
# Allow users and admins by user IDs (comma separated list)
|
||||||
LNBITS_ALLOWED_USERS=""
|
LNBITS_ALLOWED_USERS=""
|
||||||
LNBITS_ADMIN_USERS=""
|
LNBITS_ADMIN_USERS=""
|
||||||
# Extensions only admin can access
|
# Extensions only admin can access
|
||||||
LNBITS_ADMIN_EXTENSIONS="ngrok"
|
LNBITS_ADMIN_EXTENSIONS="ngrok"
|
||||||
LNBITS_DEFAULT_WALLET_NAME="LNbits wallet"
|
LNBITS_DEFAULT_WALLET_NAME="LNbits wallet"
|
||||||
|
|
||||||
# csv ad image filepaths or urls, extensions can choose to honor
|
# Ad space description
|
||||||
LNBITS_AD_SPACE=""
|
# LNBITS_AD_SPACE_TITLE="Supported by"
|
||||||
|
# csv ad space, format "<url>;<img-light>;<img-dark>, <url>;<img-light>;<img-dark>", extensions can choose to honor
|
||||||
|
# LNBITS_AD_SPACE=""
|
||||||
|
|
||||||
# Hides wallet api, extensions can choose to honor
|
# Hides wallet api, extensions can choose to honor
|
||||||
LNBITS_HIDE_API=false
|
LNBITS_HIDE_API=false
|
||||||
|
|
|
||||||
31
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
31
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
title: "[BUG]"
|
||||||
|
labels: bug
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
**To Reproduce**
|
||||||
|
Steps to reproduce the behavior:
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '....'
|
||||||
|
3. Scroll down to '....'
|
||||||
|
4. See error
|
||||||
|
|
||||||
|
**Expected behavior**
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
**Screenshots**
|
||||||
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
|
**Desktop (please complete the following information):**
|
||||||
|
- LNbits version: [e.g. 0.9.2 or commit hash]
|
||||||
|
- Database [e.g. sqlite, postgres]
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context about the problem here.
|
||||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an idea for this project
|
||||||
|
title: "[Feature request]"
|
||||||
|
labels: feature request
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Is your feature request related to a problem? Please describe.**
|
||||||
|
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||||
|
|
||||||
|
**Describe the solution you'd like**
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
|
**Describe alternatives you've considered**
|
||||||
|
A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context or screenshots about the feature request here.
|
||||||
10
.github/ISSUE_TEMPLATE/something-else.md
vendored
Normal file
10
.github/ISSUE_TEMPLATE/something-else.md
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
---
|
||||||
|
name: Something else
|
||||||
|
about: Anything else that you need to say
|
||||||
|
title: ''
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -8,6 +8,7 @@ RUN curl -sSL https://install.python-poetry.org | python3 -
|
||||||
ENV PATH="/root/.local/bin:$PATH"
|
ENV PATH="/root/.local/bin:$PATH"
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
RUN mkdir -p lnbits/data
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,8 @@ poetry run lnbits
|
||||||
|
|
||||||
## Option 2: Nix
|
## Option 2: Nix
|
||||||
|
|
||||||
|
> note: currently not supported while we make some architectural changes on the path to leave beta
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git clone https://github.com/lnbits/lnbits-legend.git
|
git clone https://github.com/lnbits/lnbits-legend.git
|
||||||
cd lnbits-legend/
|
cd lnbits-legend/
|
||||||
|
|
@ -155,6 +157,7 @@ kill_timeout = 30
|
||||||
HOST="127.0.0.1"
|
HOST="127.0.0.1"
|
||||||
PORT=5000
|
PORT=5000
|
||||||
LNBITS_FORCE_HTTPS=true
|
LNBITS_FORCE_HTTPS=true
|
||||||
|
FORWARDED_ALLOW_IPS="*"
|
||||||
LNBITS_DATA_FOLDER="/data"
|
LNBITS_DATA_FOLDER="/data"
|
||||||
|
|
||||||
${PUT_YOUR_LNBITS_ENV_VARS_HERE}
|
${PUT_YOUR_LNBITS_ENV_VARS_HERE}
|
||||||
|
|
@ -217,8 +220,8 @@ You need to edit the `.env` file.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# add the database connection string to .env 'nano .env' LNBITS_DATABASE_URL=
|
# add the database connection string to .env 'nano .env' LNBITS_DATABASE_URL=
|
||||||
# postgres://<user>:<myPassword>@<host>/<lnbits> - alter line bellow with your user, password and db name
|
# postgres://<user>:<myPassword>@<host>:<port>/<lnbits> - alter line bellow with your user, password and db name
|
||||||
LNBITS_DATABASE_URL="postgres://postgres:postgres@localhost/lnbits"
|
LNBITS_DATABASE_URL="postgres://postgres:postgres@localhost:5432/lnbits"
|
||||||
# save and exit
|
# save and exit
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -79,3 +79,8 @@ For the invoice to work you must have a publicly accessible URL in your LNbits.
|
||||||
- `LNBITS_BACKEND_WALLET_CLASS`: **OpenNodeWallet**
|
- `LNBITS_BACKEND_WALLET_CLASS`: **OpenNodeWallet**
|
||||||
- `OPENNODE_API_ENDPOINT`: https://api.opennode.com/
|
- `OPENNODE_API_ENDPOINT`: https://api.opennode.com/
|
||||||
- `OPENNODE_KEY`: opennodeAdminApiKey
|
- `OPENNODE_KEY`: opennodeAdminApiKey
|
||||||
|
|
||||||
|
|
||||||
|
### Cliche Wallet
|
||||||
|
|
||||||
|
- `CLICHE_ENDPOINT`: ws://127.0.0.1:12000
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,6 @@ def create_app(config_object="lnbits.settings") -> FastAPI:
|
||||||
)
|
)
|
||||||
|
|
||||||
app.add_middleware(GZipMiddleware, minimum_size=1000)
|
app.add_middleware(GZipMiddleware, minimum_size=1000)
|
||||||
# app.add_middleware(ASGIProxyFix)
|
|
||||||
|
|
||||||
check_funding_source(app)
|
check_funding_source(app)
|
||||||
register_assets(app)
|
register_assets(app)
|
||||||
|
|
|
||||||
|
|
@ -229,6 +229,24 @@ async def get_wallet_payment(
|
||||||
return Payment.from_row(row) if row else None
|
return Payment.from_row(row) if row else None
|
||||||
|
|
||||||
|
|
||||||
|
async def get_latest_payments_by_extension(ext_name: str, ext_id: str, limit: int = 5):
|
||||||
|
rows = await db.fetchall(
|
||||||
|
f"""
|
||||||
|
SELECT * FROM apipayments
|
||||||
|
WHERE pending = 'false'
|
||||||
|
AND extra LIKE ?
|
||||||
|
AND extra LIKE ?
|
||||||
|
ORDER BY time DESC LIMIT {limit}
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
f"%{ext_name}%",
|
||||||
|
f"%{ext_id}%",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
async def get_payments(
|
async def get_payments(
|
||||||
*,
|
*,
|
||||||
wallet_id: Optional[str] = None,
|
wallet_id: Optional[str] = None,
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ async def m001_initial(db):
|
||||||
f"""
|
f"""
|
||||||
CREATE TABLE IF NOT EXISTS apipayments (
|
CREATE TABLE IF NOT EXISTS apipayments (
|
||||||
payhash TEXT NOT NULL,
|
payhash TEXT NOT NULL,
|
||||||
amount INTEGER NOT NULL,
|
amount {db.big_int} NOT NULL,
|
||||||
fee INTEGER NOT NULL DEFAULT 0,
|
fee INTEGER NOT NULL DEFAULT 0,
|
||||||
wallet TEXT NOT NULL,
|
wallet TEXT NOT NULL,
|
||||||
pending BOOLEAN NOT NULL,
|
pending BOOLEAN NOT NULL,
|
||||||
|
|
|
||||||
|
|
@ -361,6 +361,35 @@ new Vue({
|
||||||
this.receive.status = 'pending'
|
this.receive.status = 'pending'
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
onInitQR: async function (promise) {
|
||||||
|
try {
|
||||||
|
await promise
|
||||||
|
} catch (error) {
|
||||||
|
let mapping = {
|
||||||
|
NotAllowedError: 'ERROR: you need to grant camera access permission',
|
||||||
|
NotFoundError: 'ERROR: no camera on this device',
|
||||||
|
NotSupportedError:
|
||||||
|
'ERROR: secure context required (HTTPS, localhost)',
|
||||||
|
NotReadableError: 'ERROR: is the camera already in use?',
|
||||||
|
OverconstrainedError: 'ERROR: installed cameras are not suitable',
|
||||||
|
StreamApiNotSupportedError:
|
||||||
|
'ERROR: Stream API is not supported in this browser',
|
||||||
|
InsecureContextError:
|
||||||
|
'ERROR: Camera access is only permitted in secure context. Use HTTPS or localhost rather than HTTP.'
|
||||||
|
}
|
||||||
|
let valid_error = Object.keys(mapping).filter(key => {
|
||||||
|
return error.name === key
|
||||||
|
})
|
||||||
|
let camera_error = valid_error
|
||||||
|
? mapping[valid_error]
|
||||||
|
: `ERROR: Camera error (${error.name})`
|
||||||
|
this.parse.camera.show = false
|
||||||
|
this.$q.notify({
|
||||||
|
message: camera_error,
|
||||||
|
type: 'negative'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
decodeQR: function (res) {
|
decodeQR: function (res) {
|
||||||
this.parse.data.request = res
|
this.parse.data.request = res
|
||||||
this.decodeRequest()
|
this.decodeRequest()
|
||||||
|
|
|
||||||
|
|
@ -183,6 +183,23 @@
|
||||||
<div class="col q-pl-md"> </div>
|
<div class="col q-pl-md"> </div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if AD_SPACE %} {% for ADS in AD_SPACE %} {% set AD = ADS.split(';') %}
|
||||||
|
<div class="col-6 col-sm-4 col-md-8 q-gutter-y-sm">
|
||||||
|
<q-btn flat color="secondary" class="full-width q-mb-md"
|
||||||
|
>{{ AD_TITLE }}</q-btn
|
||||||
|
>
|
||||||
|
|
||||||
|
<a href="{{ AD[0] }}" class="q-ma-md">
|
||||||
|
<img
|
||||||
|
v-if="($q.dark.isActive)"
|
||||||
|
src="{{ AD[1] }}"
|
||||||
|
style="max-width: 90%"
|
||||||
|
/>
|
||||||
|
<img v-else src="{{ AD[2] }}" style="max-width: 90%" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endfor %} {% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -388,9 +388,14 @@
|
||||||
{% endif %} {% if AD_SPACE %} {% for ADS in AD_SPACE %} {% set AD =
|
{% endif %} {% if AD_SPACE %} {% for ADS in AD_SPACE %} {% set AD =
|
||||||
ADS.split(';') %}
|
ADS.split(';') %}
|
||||||
<q-card>
|
<q-card>
|
||||||
<a href="{{ AD[0] }}"
|
<q-card-section>
|
||||||
><img width="100%" src="{{ AD[1] }}"
|
<h6 class="text-subtitle1 q-mt-none q-mb-sm">{{ AD_TITLE }}</h6>
|
||||||
/></a> </q-card
|
</q-card-section>
|
||||||
|
<q-card-section class="q-pa-none">
|
||||||
|
<a href="{{ AD[0] }}" class="q-ma-md">
|
||||||
|
<img v-if="($q.dark.isActive)" src="{{ AD[1] }}" />
|
||||||
|
<img v-else src="{{ AD[2] }}" />
|
||||||
|
</a> </q-card-section></q-card
|
||||||
>{% endfor %} {% endif %}
|
>{% endfor %} {% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -653,6 +658,7 @@
|
||||||
<q-responsive :ratio="1">
|
<q-responsive :ratio="1">
|
||||||
<qrcode-stream
|
<qrcode-stream
|
||||||
@decode="decodeQR"
|
@decode="decodeQR"
|
||||||
|
@init="onInitQR"
|
||||||
class="rounded-borders"
|
class="rounded-borders"
|
||||||
></qrcode-stream>
|
></qrcode-stream>
|
||||||
</q-responsive>
|
</q-responsive>
|
||||||
|
|
@ -671,6 +677,7 @@
|
||||||
<div class="text-center q-mb-lg">
|
<div class="text-center q-mb-lg">
|
||||||
<qrcode-stream
|
<qrcode-stream
|
||||||
@decode="decodeQR"
|
@decode="decodeQR"
|
||||||
|
@init="onInitQR"
|
||||||
class="rounded-borders"
|
class="rounded-borders"
|
||||||
></qrcode-stream>
|
></qrcode-stream>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -155,30 +155,29 @@ class CreateInvoiceData(BaseModel):
|
||||||
|
|
||||||
|
|
||||||
async def api_payments_create_invoice(data: CreateInvoiceData, wallet: Wallet):
|
async def api_payments_create_invoice(data: CreateInvoiceData, wallet: Wallet):
|
||||||
if data.description_hash:
|
if data.description_hash or data.unhashed_description:
|
||||||
try:
|
try:
|
||||||
description_hash = binascii.unhexlify(data.description_hash)
|
description_hash = (
|
||||||
|
binascii.unhexlify(data.description_hash)
|
||||||
|
if data.description_hash
|
||||||
|
else b""
|
||||||
|
)
|
||||||
|
unhashed_description = (
|
||||||
|
binascii.unhexlify(data.unhashed_description)
|
||||||
|
if data.unhashed_description
|
||||||
|
else b""
|
||||||
|
)
|
||||||
except binascii.Error:
|
except binascii.Error:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
detail="'description_hash' must be a valid hex string",
|
detail="'description_hash' and 'unhashed_description' must be a valid hex strings",
|
||||||
)
|
)
|
||||||
unhashed_description = b""
|
|
||||||
memo = ""
|
|
||||||
elif data.unhashed_description:
|
|
||||||
try:
|
|
||||||
unhashed_description = binascii.unhexlify(data.unhashed_description)
|
|
||||||
except binascii.Error:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
|
||||||
detail="'unhashed_description' must be a valid hex string",
|
|
||||||
)
|
|
||||||
description_hash = b""
|
|
||||||
memo = ""
|
memo = ""
|
||||||
else:
|
else:
|
||||||
description_hash = b""
|
description_hash = b""
|
||||||
unhashed_description = b""
|
unhashed_description = b""
|
||||||
memo = data.memo or LNBITS_SITE_TITLE
|
memo = data.memo or LNBITS_SITE_TITLE
|
||||||
|
|
||||||
if data.unit == "sat":
|
if data.unit == "sat":
|
||||||
amount = int(data.amount)
|
amount = int(data.amount)
|
||||||
else:
|
else:
|
||||||
|
|
@ -476,7 +475,7 @@ async def api_lnurlscan(code: str, wallet: WalletTypeInfo = Depends(get_key_type
|
||||||
except:
|
except:
|
||||||
# parse internet identifier (user@domain.com)
|
# parse internet identifier (user@domain.com)
|
||||||
name_domain = code.split("@")
|
name_domain = code.split("@")
|
||||||
if len(name_domain) == 2 and len(name_domain[1].split(".")) == 2:
|
if len(name_domain) == 2 and len(name_domain[1].split(".")) >= 2:
|
||||||
name, domain = name_domain
|
name, domain = name_domain
|
||||||
url = (
|
url = (
|
||||||
("http://" if domain.endswith(".onion") else "https://")
|
("http://" if domain.endswith(".onion") else "https://")
|
||||||
|
|
|
||||||
|
|
@ -95,4 +95,4 @@ async def api_bleskomat_delete(
|
||||||
)
|
)
|
||||||
|
|
||||||
await delete_bleskomat(bleskomat_id)
|
await delete_bleskomat(bleskomat_id)
|
||||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
return "", HTTPStatus.NO_CONTENT
|
||||||
|
|
|
||||||
|
|
@ -380,7 +380,11 @@
|
||||||
<strong>Lock key:</strong> {{ qrCodeDialog.data.k0 }}<br />
|
<strong>Lock key:</strong> {{ qrCodeDialog.data.k0 }}<br />
|
||||||
<strong>Meta key:</strong> {{ qrCodeDialog.data.k1 }}<br />
|
<strong>Meta key:</strong> {{ qrCodeDialog.data.k1 }}<br />
|
||||||
<strong>File key:</strong> {{ qrCodeDialog.data.k2 }}<br />
|
<strong>File key:</strong> {{ qrCodeDialog.data.k2 }}<br />
|
||||||
|
<br />
|
||||||
|
Always backup all keys that you're trying to write on the card. Without
|
||||||
|
them you may not be able to change them in the future!<br />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
<q-btn
|
<q-btn
|
||||||
unelevated
|
unelevated
|
||||||
|
|
|
||||||
|
|
@ -129,7 +129,7 @@ async def api_card_delete(card_id, wallet: WalletTypeInfo = Depends(require_admi
|
||||||
raise HTTPException(detail="Not your card.", status_code=HTTPStatus.FORBIDDEN)
|
raise HTTPException(detail="Not your card.", status_code=HTTPStatus.FORBIDDEN)
|
||||||
|
|
||||||
await delete_card(card_id)
|
await delete_card(card_id)
|
||||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
return "", HTTPStatus.NO_CONTENT
|
||||||
|
|
||||||
|
|
||||||
@boltcards_ext.get("/api/v1/hits")
|
@boltcards_ext.get("/api/v1/hits")
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ async def check_for_pending_swaps():
|
||||||
swap_status = get_swap_status(swap)
|
swap_status = get_swap_status(swap)
|
||||||
# should only happen while development when regtest is reset
|
# should only happen while development when regtest is reset
|
||||||
if swap_status.exists is False:
|
if swap_status.exists is False:
|
||||||
logger.warning(f"Boltz - swap: {swap.boltz_id} does not exist.")
|
logger.debug(f"Boltz - swap: {swap.boltz_id} does not exist.")
|
||||||
await update_swap_status(swap.id, "failed")
|
await update_swap_status(swap.id, "failed")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|
@ -73,7 +73,7 @@ async def check_for_pending_swaps():
|
||||||
else:
|
else:
|
||||||
if swap_status.hit_timeout:
|
if swap_status.hit_timeout:
|
||||||
if not swap_status.has_lockup:
|
if not swap_status.has_lockup:
|
||||||
logger.warning(
|
logger.debug(
|
||||||
f"Boltz - swap: {swap.id} hit timeout, but no lockup tx..."
|
f"Boltz - swap: {swap.id} hit timeout, but no lockup tx..."
|
||||||
)
|
)
|
||||||
await update_swap_status(swap.id, "timeout")
|
await update_swap_status(swap.id, "timeout")
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ from .models import Copilots, CreateCopilotData
|
||||||
|
|
||||||
async def create_copilot(
|
async def create_copilot(
|
||||||
data: CreateCopilotData, inkey: Optional[str] = ""
|
data: CreateCopilotData, inkey: Optional[str] = ""
|
||||||
) -> Copilots:
|
) -> Optional[Copilots]:
|
||||||
copilot_id = urlsafe_short_hash()
|
copilot_id = urlsafe_short_hash()
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"""
|
"""
|
||||||
|
|
@ -67,19 +67,19 @@ async def create_copilot(
|
||||||
|
|
||||||
|
|
||||||
async def update_copilot(
|
async def update_copilot(
|
||||||
data: CreateCopilotData, copilot_id: Optional[str] = ""
|
data: CreateCopilotData, copilot_id: str
|
||||||
) -> Optional[Copilots]:
|
) -> Optional[Copilots]:
|
||||||
q = ", ".join([f"{field[0]} = ?" for field in data])
|
q = ", ".join([f"{field[0]} = ?" for field in data])
|
||||||
items = [f"{field[1]}" for field in data]
|
items = [f"{field[1]}" for field in data]
|
||||||
items.append(copilot_id)
|
items.append(copilot_id)
|
||||||
await db.execute(f"UPDATE copilot.newer_copilots SET {q} WHERE id = ?", (items))
|
await db.execute(f"UPDATE copilot.newer_copilots SET {q} WHERE id = ?", (items,))
|
||||||
row = await db.fetchone(
|
row = await db.fetchone(
|
||||||
"SELECT * FROM copilot.newer_copilots WHERE id = ?", (copilot_id,)
|
"SELECT * FROM copilot.newer_copilots WHERE id = ?", (copilot_id,)
|
||||||
)
|
)
|
||||||
return Copilots(**row) if row else None
|
return Copilots(**row) if row else None
|
||||||
|
|
||||||
|
|
||||||
async def get_copilot(copilot_id: str) -> Copilots:
|
async def get_copilot(copilot_id: str) -> Optional[Copilots]:
|
||||||
row = await db.fetchone(
|
row = await db.fetchone(
|
||||||
"SELECT * FROM copilot.newer_copilots WHERE id = ?", (copilot_id,)
|
"SELECT * FROM copilot.newer_copilots WHERE id = ?", (copilot_id,)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ async def wait_for_paid_invoices():
|
||||||
async def on_invoice_paid(payment: Payment) -> None:
|
async def on_invoice_paid(payment: Payment) -> None:
|
||||||
webhook = None
|
webhook = None
|
||||||
data = None
|
data = None
|
||||||
if payment.extra.get("tag") != "copilot":
|
if not payment.extra or payment.extra.get("tag") != "copilot":
|
||||||
# not an copilot invoice
|
# not an copilot invoice
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
@ -71,8 +71,8 @@ async def on_invoice_paid(payment: Payment) -> None:
|
||||||
|
|
||||||
|
|
||||||
async def mark_webhook_sent(payment: Payment, status: int) -> None:
|
async def mark_webhook_sent(payment: Payment, status: int) -> None:
|
||||||
|
if payment.extra:
|
||||||
payment.extra["wh_status"] = status
|
payment.extra["wh_status"] = status
|
||||||
|
|
||||||
await core_db.execute(
|
await core_db.execute(
|
||||||
"""
|
"""
|
||||||
UPDATE apipayments SET extra = ?
|
UPDATE apipayments SET extra = ?
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,9 @@ templates = Jinja2Templates(directory="templates")
|
||||||
|
|
||||||
|
|
||||||
@copilot_ext.get("/", response_class=HTMLResponse)
|
@copilot_ext.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) # type: ignore
|
||||||
|
):
|
||||||
return copilot_renderer().TemplateResponse(
|
return copilot_renderer().TemplateResponse(
|
||||||
"copilot/index.html", {"request": request, "user": user.dict()}
|
"copilot/index.html", {"request": request, "user": user.dict()}
|
||||||
)
|
)
|
||||||
|
|
@ -44,7 +46,7 @@ class ConnectionManager:
|
||||||
|
|
||||||
async def connect(self, websocket: WebSocket, copilot_id: str):
|
async def connect(self, websocket: WebSocket, copilot_id: str):
|
||||||
await websocket.accept()
|
await websocket.accept()
|
||||||
websocket.id = copilot_id
|
websocket.id = copilot_id # type: ignore
|
||||||
self.active_connections.append(websocket)
|
self.active_connections.append(websocket)
|
||||||
|
|
||||||
def disconnect(self, websocket: WebSocket):
|
def disconnect(self, websocket: WebSocket):
|
||||||
|
|
@ -52,7 +54,7 @@ class ConnectionManager:
|
||||||
|
|
||||||
async def send_personal_message(self, message: str, copilot_id: str):
|
async def send_personal_message(self, message: str, copilot_id: str):
|
||||||
for connection in self.active_connections:
|
for connection in self.active_connections:
|
||||||
if connection.id == copilot_id:
|
if connection.id == copilot_id: # type: ignore
|
||||||
await connection.send_text(message)
|
await connection.send_text(message)
|
||||||
|
|
||||||
async def broadcast(self, message: str):
|
async def broadcast(self, message: str):
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ from .views import updater
|
||||||
|
|
||||||
@copilot_ext.get("/api/v1/copilot")
|
@copilot_ext.get("/api/v1/copilot")
|
||||||
async def api_copilots_retrieve(
|
async def api_copilots_retrieve(
|
||||||
req: Request, wallet: WalletTypeInfo = Depends(get_key_type)
|
req: Request, wallet: WalletTypeInfo = Depends(get_key_type) # type: ignore
|
||||||
):
|
):
|
||||||
wallet_user = wallet.wallet.user
|
wallet_user = wallet.wallet.user
|
||||||
copilots = [copilot.dict() for copilot in await get_copilots(wallet_user)]
|
copilots = [copilot.dict() for copilot in await get_copilots(wallet_user)]
|
||||||
|
|
@ -37,7 +37,7 @@ async def api_copilots_retrieve(
|
||||||
async def api_copilot_retrieve(
|
async def api_copilot_retrieve(
|
||||||
req: Request,
|
req: Request,
|
||||||
copilot_id: str = Query(None),
|
copilot_id: str = Query(None),
|
||||||
wallet: WalletTypeInfo = Depends(get_key_type),
|
wallet: WalletTypeInfo = Depends(get_key_type), # type: ignore
|
||||||
):
|
):
|
||||||
copilot = await get_copilot(copilot_id)
|
copilot = await get_copilot(copilot_id)
|
||||||
if not copilot:
|
if not copilot:
|
||||||
|
|
@ -54,7 +54,7 @@ async def api_copilot_retrieve(
|
||||||
async def api_copilot_create_or_update(
|
async def api_copilot_create_or_update(
|
||||||
data: CreateCopilotData,
|
data: CreateCopilotData,
|
||||||
copilot_id: str = Query(None),
|
copilot_id: str = Query(None),
|
||||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
wallet: WalletTypeInfo = Depends(require_admin_key), # type: ignore
|
||||||
):
|
):
|
||||||
data.user = wallet.wallet.user
|
data.user = wallet.wallet.user
|
||||||
data.wallet = wallet.wallet.id
|
data.wallet = wallet.wallet.id
|
||||||
|
|
@ -67,7 +67,8 @@ async def api_copilot_create_or_update(
|
||||||
|
|
||||||
@copilot_ext.delete("/api/v1/copilot/{copilot_id}")
|
@copilot_ext.delete("/api/v1/copilot/{copilot_id}")
|
||||||
async def api_copilot_delete(
|
async def api_copilot_delete(
|
||||||
copilot_id: str = Query(None), wallet: WalletTypeInfo = Depends(require_admin_key)
|
copilot_id: str = Query(None),
|
||||||
|
wallet: WalletTypeInfo = Depends(require_admin_key), # type: ignore
|
||||||
):
|
):
|
||||||
copilot = await get_copilot(copilot_id)
|
copilot = await get_copilot(copilot_id)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -98,21 +98,21 @@ async def get_discordbot_wallet(wallet_id: str) -> Optional[Wallets]:
|
||||||
return Wallets(**row) if row else None
|
return Wallets(**row) if row else None
|
||||||
|
|
||||||
|
|
||||||
async def get_discordbot_wallets(admin_id: str) -> Optional[Wallets]:
|
async def get_discordbot_wallets(admin_id: str) -> List[Wallets]:
|
||||||
rows = await db.fetchall(
|
rows = await db.fetchall(
|
||||||
"SELECT * FROM discordbot.wallets WHERE admin = ?", (admin_id,)
|
"SELECT * FROM discordbot.wallets WHERE admin = ?", (admin_id,)
|
||||||
)
|
)
|
||||||
return [Wallets(**row) for row in rows]
|
return [Wallets(**row) for row in rows]
|
||||||
|
|
||||||
|
|
||||||
async def get_discordbot_users_wallets(user_id: str) -> Optional[Wallets]:
|
async def get_discordbot_users_wallets(user_id: str) -> List[Wallets]:
|
||||||
rows = await db.fetchall(
|
rows = await db.fetchall(
|
||||||
"""SELECT * FROM discordbot.wallets WHERE "user" = ?""", (user_id,)
|
"""SELECT * FROM discordbot.wallets WHERE "user" = ?""", (user_id,)
|
||||||
)
|
)
|
||||||
return [Wallets(**row) for row in rows]
|
return [Wallets(**row) for row in rows]
|
||||||
|
|
||||||
|
|
||||||
async def get_discordbot_wallet_transactions(wallet_id: str) -> Optional[Payment]:
|
async def get_discordbot_wallet_transactions(wallet_id: str) -> List[Payment]:
|
||||||
return await get_payments(
|
return await get_payments(
|
||||||
wallet_id=wallet_id, complete=True, pending=False, outgoing=True, incoming=True
|
wallet_id=wallet_id, complete=True, pending=False, outgoing=True, incoming=True
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,9 @@ from . import discordbot_ext, discordbot_renderer
|
||||||
|
|
||||||
|
|
||||||
@discordbot_ext.get("/", response_class=HTMLResponse)
|
@discordbot_ext.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) # type: ignore
|
||||||
|
):
|
||||||
return discordbot_renderer().TemplateResponse(
|
return discordbot_renderer().TemplateResponse(
|
||||||
"discordbot/index.html", {"request": request, "user": user.dict()}
|
"discordbot/index.html", {"request": request, "user": user.dict()}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -27,32 +27,37 @@ from .models import CreateUserData, CreateUserWallet
|
||||||
|
|
||||||
|
|
||||||
@discordbot_ext.get("/api/v1/users", status_code=HTTPStatus.OK)
|
@discordbot_ext.get("/api/v1/users", status_code=HTTPStatus.OK)
|
||||||
async def api_discordbot_users(wallet: WalletTypeInfo = Depends(get_key_type)):
|
async def api_discordbot_users(
|
||||||
|
wallet: WalletTypeInfo = Depends(get_key_type), # type: ignore
|
||||||
|
):
|
||||||
user_id = wallet.wallet.user
|
user_id = wallet.wallet.user
|
||||||
return [user.dict() for user in await get_discordbot_users(user_id)]
|
return [user.dict() for user in await get_discordbot_users(user_id)]
|
||||||
|
|
||||||
|
|
||||||
@discordbot_ext.get("/api/v1/users/{user_id}", status_code=HTTPStatus.OK)
|
@discordbot_ext.get("/api/v1/users/{user_id}", status_code=HTTPStatus.OK)
|
||||||
async def api_discordbot_user(user_id, wallet: WalletTypeInfo = Depends(get_key_type)):
|
async def api_discordbot_user(
|
||||||
|
user_id, wallet: WalletTypeInfo = Depends(get_key_type) # type: ignore
|
||||||
|
):
|
||||||
user = await get_discordbot_user(user_id)
|
user = await get_discordbot_user(user_id)
|
||||||
|
if user:
|
||||||
return user.dict()
|
return user.dict()
|
||||||
|
|
||||||
|
|
||||||
@discordbot_ext.post("/api/v1/users", status_code=HTTPStatus.CREATED)
|
@discordbot_ext.post("/api/v1/users", status_code=HTTPStatus.CREATED)
|
||||||
async def api_discordbot_users_create(
|
async def api_discordbot_users_create(
|
||||||
data: CreateUserData, wallet: WalletTypeInfo = Depends(get_key_type)
|
data: CreateUserData, wallet: WalletTypeInfo = Depends(get_key_type) # type: ignore
|
||||||
):
|
):
|
||||||
user = await create_discordbot_user(data)
|
user = await create_discordbot_user(data)
|
||||||
full = user.dict()
|
full = user.dict()
|
||||||
full["wallets"] = [
|
wallets = await get_discordbot_users_wallets(user.id)
|
||||||
wallet.dict() for wallet in await get_discordbot_users_wallets(user.id)
|
if wallets:
|
||||||
]
|
full["wallets"] = [wallet for wallet in wallets]
|
||||||
return full
|
return full
|
||||||
|
|
||||||
|
|
||||||
@discordbot_ext.delete("/api/v1/users/{user_id}")
|
@discordbot_ext.delete("/api/v1/users/{user_id}")
|
||||||
async def api_discordbot_users_delete(
|
async def api_discordbot_users_delete(
|
||||||
user_id, wallet: WalletTypeInfo = Depends(get_key_type)
|
user_id, wallet: WalletTypeInfo = Depends(get_key_type) # type: ignore
|
||||||
):
|
):
|
||||||
user = await get_discordbot_user(user_id)
|
user = await get_discordbot_user(user_id)
|
||||||
if not user:
|
if not user:
|
||||||
|
|
@ -60,7 +65,7 @@ async def api_discordbot_users_delete(
|
||||||
status_code=HTTPStatus.NOT_FOUND, detail="User does not exist."
|
status_code=HTTPStatus.NOT_FOUND, detail="User does not exist."
|
||||||
)
|
)
|
||||||
await delete_discordbot_user(user_id)
|
await delete_discordbot_user(user_id)
|
||||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
return "", HTTPStatus.NO_CONTENT
|
||||||
|
|
||||||
|
|
||||||
# Activate Extension
|
# Activate Extension
|
||||||
|
|
@ -75,7 +80,7 @@ async def api_discordbot_activate_extension(
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.NOT_FOUND, detail="User does not exist."
|
status_code=HTTPStatus.NOT_FOUND, detail="User does not exist."
|
||||||
)
|
)
|
||||||
update_user_extension(user_id=userid, extension=extension, active=active)
|
await update_user_extension(user_id=userid, extension=extension, active=active)
|
||||||
return {"extension": "updated"}
|
return {"extension": "updated"}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -84,7 +89,7 @@ async def api_discordbot_activate_extension(
|
||||||
|
|
||||||
@discordbot_ext.post("/api/v1/wallets")
|
@discordbot_ext.post("/api/v1/wallets")
|
||||||
async def api_discordbot_wallets_create(
|
async def api_discordbot_wallets_create(
|
||||||
data: CreateUserWallet, wallet: WalletTypeInfo = Depends(get_key_type)
|
data: CreateUserWallet, wallet: WalletTypeInfo = Depends(get_key_type) # type: ignore
|
||||||
):
|
):
|
||||||
user = await create_discordbot_wallet(
|
user = await create_discordbot_wallet(
|
||||||
user_id=data.user_id, wallet_name=data.wallet_name, admin_id=data.admin_id
|
user_id=data.user_id, wallet_name=data.wallet_name, admin_id=data.admin_id
|
||||||
|
|
@ -93,28 +98,30 @@ async def api_discordbot_wallets_create(
|
||||||
|
|
||||||
|
|
||||||
@discordbot_ext.get("/api/v1/wallets")
|
@discordbot_ext.get("/api/v1/wallets")
|
||||||
async def api_discordbot_wallets(wallet: WalletTypeInfo = Depends(get_key_type)):
|
async def api_discordbot_wallets(
|
||||||
|
wallet: WalletTypeInfo = Depends(get_key_type), # type: ignore
|
||||||
|
):
|
||||||
admin_id = wallet.wallet.user
|
admin_id = wallet.wallet.user
|
||||||
return [wallet.dict() for wallet in await get_discordbot_wallets(admin_id)]
|
return await get_discordbot_wallets(admin_id)
|
||||||
|
|
||||||
|
|
||||||
@discordbot_ext.get("/api/v1/transactions/{wallet_id}")
|
@discordbot_ext.get("/api/v1/transactions/{wallet_id}")
|
||||||
async def api_discordbot_wallet_transactions(
|
async def api_discordbot_wallet_transactions(
|
||||||
wallet_id, wallet: WalletTypeInfo = Depends(get_key_type)
|
wallet_id, wallet: WalletTypeInfo = Depends(get_key_type) # type: ignore
|
||||||
):
|
):
|
||||||
return await get_discordbot_wallet_transactions(wallet_id)
|
return await get_discordbot_wallet_transactions(wallet_id)
|
||||||
|
|
||||||
|
|
||||||
@discordbot_ext.get("/api/v1/wallets/{user_id}")
|
@discordbot_ext.get("/api/v1/wallets/{user_id}")
|
||||||
async def api_discordbot_users_wallets(
|
async def api_discordbot_users_wallets(
|
||||||
user_id, wallet: WalletTypeInfo = Depends(get_key_type)
|
user_id, wallet: WalletTypeInfo = Depends(get_key_type) # type: ignore
|
||||||
):
|
):
|
||||||
return [s_wallet.dict() for s_wallet in await get_discordbot_users_wallets(user_id)]
|
return await get_discordbot_users_wallets(user_id)
|
||||||
|
|
||||||
|
|
||||||
@discordbot_ext.delete("/api/v1/wallets/{wallet_id}")
|
@discordbot_ext.delete("/api/v1/wallets/{wallet_id}")
|
||||||
async def api_discordbot_wallets_delete(
|
async def api_discordbot_wallets_delete(
|
||||||
wallet_id, wallet: WalletTypeInfo = Depends(get_key_type)
|
wallet_id, wallet: WalletTypeInfo = Depends(get_key_type) # type: ignore
|
||||||
):
|
):
|
||||||
get_wallet = await get_discordbot_wallet(wallet_id)
|
get_wallet = await get_discordbot_wallet(wallet_id)
|
||||||
if not get_wallet:
|
if not get_wallet:
|
||||||
|
|
@ -122,4 +129,4 @@ async def api_discordbot_wallets_delete(
|
||||||
status_code=HTTPStatus.NOT_FOUND, detail="Wallet does not exist."
|
status_code=HTTPStatus.NOT_FOUND, detail="Wallet does not exist."
|
||||||
)
|
)
|
||||||
await delete_discordbot_wallet(wallet_id, get_wallet.user)
|
await delete_discordbot_wallet(wallet_id, get_wallet.user)
|
||||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
return "", HTTPStatus.NO_CONTENT
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
|
import asyncio
|
||||||
|
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
|
||||||
from lnbits.db import Database
|
from lnbits.db import Database
|
||||||
from lnbits.helpers import template_renderer
|
from lnbits.helpers import template_renderer
|
||||||
|
from lnbits.tasks import catch_everything_and_restart
|
||||||
|
|
||||||
db = Database("ext_events")
|
db = Database("ext_events")
|
||||||
|
|
||||||
|
|
@ -13,5 +16,11 @@ def events_renderer():
|
||||||
return template_renderer(["lnbits/extensions/events/templates"])
|
return template_renderer(["lnbits/extensions/events/templates"])
|
||||||
|
|
||||||
|
|
||||||
|
from .tasks import wait_for_paid_invoices
|
||||||
from .views import * # noqa
|
from .views import * # noqa
|
||||||
from .views_api import * # noqa
|
from .views_api import * # noqa
|
||||||
|
|
||||||
|
|
||||||
|
def events_start():
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
loop.create_task(catch_everything_and_restart(wait_for_paid_invoices))
|
||||||
|
|
|
||||||
39
lnbits/extensions/events/tasks.py
Normal file
39
lnbits/extensions/events/tasks.py
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
from http import HTTPStatus
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
from fastapi import HTTPException
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
from lnbits import bolt11
|
||||||
|
from lnbits.core.models import Payment
|
||||||
|
from lnbits.core.services import pay_invoice
|
||||||
|
from lnbits.extensions.events.models import CreateTicket
|
||||||
|
from lnbits.helpers import get_current_extension_name
|
||||||
|
from lnbits.tasks import register_invoice_listener
|
||||||
|
|
||||||
|
from .views_api import api_ticket_send_ticket
|
||||||
|
|
||||||
|
|
||||||
|
async def wait_for_paid_invoices():
|
||||||
|
invoice_queue = asyncio.Queue()
|
||||||
|
register_invoice_listener(invoice_queue, get_current_extension_name())
|
||||||
|
|
||||||
|
while True:
|
||||||
|
payment = await invoice_queue.get()
|
||||||
|
await on_invoice_paid(payment)
|
||||||
|
|
||||||
|
|
||||||
|
async def on_invoice_paid(payment: Payment) -> None:
|
||||||
|
# (avoid loops)
|
||||||
|
if (
|
||||||
|
"events" == payment.extra.get("tag")
|
||||||
|
and payment.extra.get("name")
|
||||||
|
and payment.extra.get("email")
|
||||||
|
):
|
||||||
|
CreateTicket.name = str(payment.extra.get("name"))
|
||||||
|
CreateTicket.email = str(payment.extra.get("email"))
|
||||||
|
await api_ticket_send_ticket(payment.memo, payment.payment_hash, CreateTicket)
|
||||||
|
return
|
||||||
|
|
@ -135,7 +135,14 @@
|
||||||
var self = this
|
var self = this
|
||||||
axios
|
axios
|
||||||
|
|
||||||
.get('/events/api/v1/tickets/' + '{{ event_id }}')
|
.get(
|
||||||
|
'/events/api/v1/tickets/' +
|
||||||
|
'{{ event_id }}' +
|
||||||
|
'/' +
|
||||||
|
self.formDialog.data.name +
|
||||||
|
'/' +
|
||||||
|
self.formDialog.data.email
|
||||||
|
)
|
||||||
.then(function (response) {
|
.then(function (response) {
|
||||||
self.paymentReq = response.data.payment_request
|
self.paymentReq = response.data.payment_request
|
||||||
self.paymentCheck = response.data.payment_hash
|
self.paymentCheck = response.data.payment_hash
|
||||||
|
|
|
||||||
|
|
@ -260,7 +260,7 @@
|
||||||
dense
|
dense
|
||||||
v-model.number="formDialog.data.price_per_ticket"
|
v-model.number="formDialog.data.price_per_ticket"
|
||||||
type="number"
|
type="number"
|
||||||
label="Price per ticket "
|
label="Sats per ticket "
|
||||||
></q-input>
|
></q-input>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ from http import HTTPStatus
|
||||||
|
|
||||||
from fastapi.param_functions import Query
|
from fastapi.param_functions import Query
|
||||||
from fastapi.params import Depends
|
from fastapi.params import Depends
|
||||||
|
from loguru import logger
|
||||||
from starlette.exceptions import HTTPException
|
from starlette.exceptions import HTTPException
|
||||||
from starlette.requests import Request
|
from starlette.requests import Request
|
||||||
|
|
||||||
|
|
@ -78,7 +79,7 @@ async def api_form_delete(event_id, wallet: WalletTypeInfo = Depends(get_key_typ
|
||||||
|
|
||||||
await delete_event(event_id)
|
await delete_event(event_id)
|
||||||
await delete_event_tickets(event_id)
|
await delete_event_tickets(event_id)
|
||||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
return "", HTTPStatus.NO_CONTENT
|
||||||
|
|
||||||
|
|
||||||
#########Tickets##########
|
#########Tickets##########
|
||||||
|
|
@ -96,8 +97,8 @@ async def api_tickets(
|
||||||
return [ticket.dict() for ticket in await get_tickets(wallet_ids)]
|
return [ticket.dict() for ticket in await get_tickets(wallet_ids)]
|
||||||
|
|
||||||
|
|
||||||
@events_ext.get("/api/v1/tickets/{event_id}")
|
@events_ext.get("/api/v1/tickets/{event_id}/{name}/{email}")
|
||||||
async def api_ticket_make_ticket(event_id):
|
async def api_ticket_make_ticket(event_id, name, email):
|
||||||
event = await get_event(event_id)
|
event = await get_event(event_id)
|
||||||
if not event:
|
if not event:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|
@ -108,11 +109,10 @@ async def api_ticket_make_ticket(event_id):
|
||||||
wallet_id=event.wallet,
|
wallet_id=event.wallet,
|
||||||
amount=event.price_per_ticket,
|
amount=event.price_per_ticket,
|
||||||
memo=f"{event_id}",
|
memo=f"{event_id}",
|
||||||
extra={"tag": "events"},
|
extra={"tag": "events", "name": name, "email": email},
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e))
|
raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e))
|
||||||
|
|
||||||
return {"payment_hash": payment_hash, "payment_request": payment_request}
|
return {"payment_hash": payment_hash, "payment_request": payment_request}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -156,7 +156,7 @@ async def api_ticket_delete(ticket_id, wallet: WalletTypeInfo = Depends(get_key_
|
||||||
)
|
)
|
||||||
|
|
||||||
await delete_ticket(ticket_id)
|
await delete_ticket(ticket_id)
|
||||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
return "", HTTPStatus.NO_CONTENT
|
||||||
|
|
||||||
|
|
||||||
# Event Tickets
|
# Event Tickets
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,10 @@ templates = Jinja2Templates(directory="templates")
|
||||||
|
|
||||||
|
|
||||||
@example_ext.get("/", response_class=HTMLResponse)
|
@example_ext.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), # type: ignore
|
||||||
|
):
|
||||||
return example_renderer().TemplateResponse(
|
return example_renderer().TemplateResponse(
|
||||||
"example/index.html", {"request": request, "user": user.dict()}
|
"example/index.html", {"request": request, "user": user.dict()}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
from typing import List, Optional
|
from typing import List, Optional, Union
|
||||||
|
|
||||||
from lnbits.helpers import urlsafe_short_hash
|
from lnbits.helpers import urlsafe_short_hash
|
||||||
|
|
||||||
|
|
@ -6,11 +6,9 @@ from . import db
|
||||||
from .models import CreateJukeboxPayment, CreateJukeLinkData, Jukebox, JukeboxPayment
|
from .models import CreateJukeboxPayment, CreateJukeLinkData, Jukebox, JukeboxPayment
|
||||||
|
|
||||||
|
|
||||||
async def create_jukebox(
|
async def create_jukebox(data: CreateJukeLinkData) -> Jukebox:
|
||||||
data: CreateJukeLinkData, inkey: Optional[str] = ""
|
|
||||||
) -> Jukebox:
|
|
||||||
juke_id = urlsafe_short_hash()
|
juke_id = urlsafe_short_hash()
|
||||||
result = await db.execute(
|
await db.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO jukebox.jukebox (id, "user", title, wallet, sp_user, sp_secret, sp_access_token, sp_refresh_token, sp_device, sp_playlists, price, profit)
|
INSERT INTO jukebox.jukebox (id, "user", title, wallet, sp_user, sp_secret, sp_access_token, sp_refresh_token, sp_device, sp_playlists, price, profit)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
|
@ -36,13 +34,13 @@ async def create_jukebox(
|
||||||
|
|
||||||
|
|
||||||
async def update_jukebox(
|
async def update_jukebox(
|
||||||
data: CreateJukeLinkData, juke_id: Optional[str] = ""
|
data: Union[CreateJukeLinkData, Jukebox], juke_id: str = ""
|
||||||
) -> Optional[Jukebox]:
|
) -> Optional[Jukebox]:
|
||||||
q = ", ".join([f"{field[0]} = ?" for field in data])
|
q = ", ".join([f"{field[0]} = ?" for field in data])
|
||||||
items = [f"{field[1]}" for field in data]
|
items = [f"{field[1]}" for field in data]
|
||||||
items.append(juke_id)
|
items.append(juke_id)
|
||||||
q = q.replace("user", '"user"', 1) # hack to make user be "user"!
|
q = q.replace("user", '"user"', 1) # hack to make user be "user"!
|
||||||
await db.execute(f"UPDATE jukebox.jukebox SET {q} WHERE id = ?", (items))
|
await db.execute(f"UPDATE jukebox.jukebox SET {q} WHERE id = ?", (items,))
|
||||||
row = await db.fetchone("SELECT * FROM jukebox.jukebox WHERE id = ?", (juke_id,))
|
row = await db.fetchone("SELECT * FROM jukebox.jukebox WHERE id = ?", (juke_id,))
|
||||||
return Jukebox(**row) if row else None
|
return Jukebox(**row) if row else None
|
||||||
|
|
||||||
|
|
@ -72,7 +70,7 @@ async def delete_jukebox(juke_id: str):
|
||||||
"""
|
"""
|
||||||
DELETE FROM jukebox.jukebox WHERE id = ?
|
DELETE FROM jukebox.jukebox WHERE id = ?
|
||||||
""",
|
""",
|
||||||
(juke_id),
|
(juke_id,),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -80,7 +78,7 @@ async def delete_jukebox(juke_id: str):
|
||||||
|
|
||||||
|
|
||||||
async def create_jukebox_payment(data: CreateJukeboxPayment) -> JukeboxPayment:
|
async def create_jukebox_payment(data: CreateJukeboxPayment) -> JukeboxPayment:
|
||||||
result = await db.execute(
|
await db.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO jukebox.jukebox_payment (payment_hash, juke_id, song_id, paid)
|
INSERT INTO jukebox.jukebox_payment (payment_hash, juke_id, song_id, paid)
|
||||||
VALUES (?, ?, ?, ?)
|
VALUES (?, ?, ?, ?)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,3 @@
|
||||||
from sqlite3 import Row
|
|
||||||
from typing import NamedTuple, Optional
|
|
||||||
|
|
||||||
from fastapi.param_functions import Query
|
from fastapi.param_functions import Query
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from pydantic.main import BaseModel
|
from pydantic.main import BaseModel
|
||||||
|
|
@ -20,19 +17,19 @@ class CreateJukeLinkData(BaseModel):
|
||||||
|
|
||||||
|
|
||||||
class Jukebox(BaseModel):
|
class Jukebox(BaseModel):
|
||||||
id: Optional[str]
|
id: str
|
||||||
user: Optional[str]
|
user: str
|
||||||
title: Optional[str]
|
title: str
|
||||||
wallet: Optional[str]
|
wallet: str
|
||||||
inkey: Optional[str]
|
inkey: str
|
||||||
sp_user: Optional[str]
|
sp_user: str
|
||||||
sp_secret: Optional[str]
|
sp_secret: str
|
||||||
sp_access_token: Optional[str]
|
sp_access_token: str
|
||||||
sp_refresh_token: Optional[str]
|
sp_refresh_token: str
|
||||||
sp_device: Optional[str]
|
sp_device: str
|
||||||
sp_playlists: Optional[str]
|
sp_playlists: str
|
||||||
price: Optional[int]
|
price: int
|
||||||
profit: Optional[int]
|
profit: int
|
||||||
|
|
||||||
|
|
||||||
class JukeboxPayment(BaseModel):
|
class JukeboxPayment(BaseModel):
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ async def wait_for_paid_invoices():
|
||||||
|
|
||||||
|
|
||||||
async def on_invoice_paid(payment: Payment) -> None:
|
async def on_invoice_paid(payment: Payment) -> None:
|
||||||
|
if payment.extra:
|
||||||
if payment.extra.get("tag") != "jukebox":
|
if payment.extra.get("tag") != "jukebox":
|
||||||
# not a jukebox invoice
|
# not a jukebox invoice
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,9 @@ templates = Jinja2Templates(directory="templates")
|
||||||
|
|
||||||
|
|
||||||
@jukebox_ext.get("/", response_class=HTMLResponse)
|
@jukebox_ext.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) # type: ignore
|
||||||
|
):
|
||||||
return jukebox_renderer().TemplateResponse(
|
return jukebox_renderer().TemplateResponse(
|
||||||
"jukebox/index.html", {"request": request, "user": user.dict()}
|
"jukebox/index.html", {"request": request, "user": user.dict()}
|
||||||
)
|
)
|
||||||
|
|
@ -31,6 +33,7 @@ async def connect_to_jukebox(request: Request, juke_id):
|
||||||
status_code=HTTPStatus.NOT_FOUND, detail="Jukebox does not exist."
|
status_code=HTTPStatus.NOT_FOUND, detail="Jukebox does not exist."
|
||||||
)
|
)
|
||||||
devices = await api_get_jukebox_device_check(juke_id)
|
devices = await api_get_jukebox_device_check(juke_id)
|
||||||
|
deviceConnected = False
|
||||||
for device in devices["devices"]:
|
for device in devices["devices"]:
|
||||||
if device["id"] == jukebox.sp_device.split("-")[1]:
|
if device["id"] == jukebox.sp_device.split("-")[1]:
|
||||||
deviceConnected = True
|
deviceConnected = True
|
||||||
|
|
@ -48,5 +51,5 @@ async def connect_to_jukebox(request: Request, juke_id):
|
||||||
else:
|
else:
|
||||||
return jukebox_renderer().TemplateResponse(
|
return jukebox_renderer().TemplateResponse(
|
||||||
"jukebox/error.html",
|
"jukebox/error.html",
|
||||||
{"request": request, "jukebox": jukebox.jukebox(req=request)},
|
{"request": request, "jukebox": jukebox.dict()},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ import json
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from fastapi import Request
|
|
||||||
from fastapi.param_functions import Query
|
from fastapi.param_functions import Query
|
||||||
from fastapi.params import Depends
|
from fastapi.params import Depends
|
||||||
from starlette.exceptions import HTTPException
|
from starlette.exceptions import HTTPException
|
||||||
|
|
@ -29,9 +28,7 @@ from .models import CreateJukeboxPayment, CreateJukeLinkData
|
||||||
|
|
||||||
@jukebox_ext.get("/api/v1/jukebox")
|
@jukebox_ext.get("/api/v1/jukebox")
|
||||||
async def api_get_jukeboxs(
|
async def api_get_jukeboxs(
|
||||||
req: Request,
|
wallet: WalletTypeInfo = Depends(require_admin_key), # type: ignore
|
||||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
|
||||||
all_wallets: bool = Query(False),
|
|
||||||
):
|
):
|
||||||
wallet_user = wallet.wallet.user
|
wallet_user = wallet.wallet.user
|
||||||
|
|
||||||
|
|
@ -53,54 +50,52 @@ async def api_check_credentials_callbac(
|
||||||
access_token: str = Query(None),
|
access_token: str = Query(None),
|
||||||
refresh_token: str = Query(None),
|
refresh_token: str = Query(None),
|
||||||
):
|
):
|
||||||
sp_code = ""
|
|
||||||
sp_access_token = ""
|
|
||||||
sp_refresh_token = ""
|
|
||||||
try:
|
|
||||||
jukebox = await get_jukebox(juke_id)
|
jukebox = await get_jukebox(juke_id)
|
||||||
except:
|
if not jukebox:
|
||||||
raise HTTPException(detail="No Jukebox", status_code=HTTPStatus.FORBIDDEN)
|
raise HTTPException(detail="No Jukebox", status_code=HTTPStatus.FORBIDDEN)
|
||||||
if code:
|
if code:
|
||||||
jukebox.sp_access_token = code
|
jukebox.sp_access_token = code
|
||||||
jukebox = await update_jukebox(jukebox, juke_id=juke_id)
|
await update_jukebox(jukebox, juke_id=juke_id)
|
||||||
if access_token:
|
if access_token:
|
||||||
jukebox.sp_access_token = access_token
|
jukebox.sp_access_token = access_token
|
||||||
jukebox.sp_refresh_token = refresh_token
|
jukebox.sp_refresh_token = refresh_token
|
||||||
jukebox = await update_jukebox(jukebox, juke_id=juke_id)
|
await update_jukebox(jukebox, juke_id=juke_id)
|
||||||
return "<h1>Success!</h1><h2>You can close this window</h2>"
|
return "<h1>Success!</h1><h2>You can close this window</h2>"
|
||||||
|
|
||||||
|
|
||||||
@jukebox_ext.get("/api/v1/jukebox/{juke_id}")
|
@jukebox_ext.get("/api/v1/jukebox/{juke_id}", dependencies=[Depends(require_admin_key)])
|
||||||
async def api_check_credentials_check(
|
async def api_check_credentials_check(juke_id: str = Query(None)):
|
||||||
juke_id: str = Query(None), wallet: WalletTypeInfo = Depends(require_admin_key)
|
|
||||||
):
|
|
||||||
jukebox = await get_jukebox(juke_id)
|
jukebox = await get_jukebox(juke_id)
|
||||||
return jukebox
|
return jukebox
|
||||||
|
|
||||||
|
|
||||||
@jukebox_ext.post("/api/v1/jukebox", status_code=HTTPStatus.CREATED)
|
@jukebox_ext.post(
|
||||||
|
"/api/v1/jukebox",
|
||||||
|
status_code=HTTPStatus.CREATED,
|
||||||
|
dependencies=[Depends(require_admin_key)],
|
||||||
|
)
|
||||||
@jukebox_ext.put("/api/v1/jukebox/{juke_id}", status_code=HTTPStatus.OK)
|
@jukebox_ext.put("/api/v1/jukebox/{juke_id}", status_code=HTTPStatus.OK)
|
||||||
async def api_create_update_jukebox(
|
async def api_create_update_jukebox(
|
||||||
data: CreateJukeLinkData,
|
data: CreateJukeLinkData, juke_id: str = Query(None)
|
||||||
juke_id: str = Query(None),
|
|
||||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
|
||||||
):
|
):
|
||||||
if juke_id:
|
if juke_id:
|
||||||
jukebox = await update_jukebox(data, juke_id=juke_id)
|
jukebox = await update_jukebox(data, juke_id=juke_id)
|
||||||
else:
|
else:
|
||||||
jukebox = await create_jukebox(data, inkey=wallet.wallet.inkey)
|
jukebox = await create_jukebox(data)
|
||||||
return jukebox
|
return jukebox
|
||||||
|
|
||||||
|
|
||||||
@jukebox_ext.delete("/api/v1/jukebox/{juke_id}")
|
@jukebox_ext.delete(
|
||||||
|
"/api/v1/jukebox/{juke_id}", dependencies=[Depends(require_admin_key)]
|
||||||
|
)
|
||||||
async def api_delete_item(
|
async def api_delete_item(
|
||||||
juke_id=None, wallet: WalletTypeInfo = Depends(require_admin_key)
|
juke_id: str = Query(None),
|
||||||
):
|
):
|
||||||
await delete_jukebox(juke_id)
|
await delete_jukebox(juke_id)
|
||||||
try:
|
# try:
|
||||||
return [{**jukebox} for jukebox in await get_jukeboxs(wallet.wallet.user)]
|
# return [{**jukebox} for jukebox in await get_jukeboxs(wallet.wallet.user)]
|
||||||
except:
|
# except:
|
||||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT, detail="No Jukebox")
|
# raise HTTPException(status_code=HTTPStatus.NO_CONTENT, detail="No Jukebox")
|
||||||
|
|
||||||
|
|
||||||
################JUKEBOX ENDPOINTS##################
|
################JUKEBOX ENDPOINTS##################
|
||||||
|
|
@ -114,9 +109,8 @@ async def api_get_jukebox_song(
|
||||||
sp_playlist: str = Query(None),
|
sp_playlist: str = Query(None),
|
||||||
retry: bool = Query(False),
|
retry: bool = Query(False),
|
||||||
):
|
):
|
||||||
try:
|
|
||||||
jukebox = await get_jukebox(juke_id)
|
jukebox = await get_jukebox(juke_id)
|
||||||
except:
|
if not jukebox:
|
||||||
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="No Jukeboxes")
|
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="No Jukeboxes")
|
||||||
tracks = []
|
tracks = []
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
|
|
@ -152,14 +146,13 @@ async def api_get_jukebox_song(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
except:
|
except:
|
||||||
something = None
|
pass
|
||||||
return [track for track in tracks]
|
return [track for track in tracks]
|
||||||
|
|
||||||
|
|
||||||
async def api_get_token(juke_id=None):
|
async def api_get_token(juke_id):
|
||||||
try:
|
|
||||||
jukebox = await get_jukebox(juke_id)
|
jukebox = await get_jukebox(juke_id)
|
||||||
except:
|
if not jukebox:
|
||||||
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="No Jukeboxes")
|
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="No Jukeboxes")
|
||||||
|
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
|
|
@ -187,7 +180,7 @@ async def api_get_token(juke_id=None):
|
||||||
jukebox.sp_access_token = r.json()["access_token"]
|
jukebox.sp_access_token = r.json()["access_token"]
|
||||||
await update_jukebox(jukebox, juke_id=juke_id)
|
await update_jukebox(jukebox, juke_id=juke_id)
|
||||||
except:
|
except:
|
||||||
something = None
|
pass
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -198,9 +191,8 @@ async def api_get_token(juke_id=None):
|
||||||
async def api_get_jukebox_device_check(
|
async def api_get_jukebox_device_check(
|
||||||
juke_id: str = Query(None), retry: bool = Query(False)
|
juke_id: str = Query(None), retry: bool = Query(False)
|
||||||
):
|
):
|
||||||
try:
|
|
||||||
jukebox = await get_jukebox(juke_id)
|
jukebox = await get_jukebox(juke_id)
|
||||||
except:
|
if not jukebox:
|
||||||
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="No Jukeboxes")
|
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="No Jukeboxes")
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
rDevice = await client.get(
|
rDevice = await client.get(
|
||||||
|
|
@ -221,7 +213,7 @@ async def api_get_jukebox_device_check(
|
||||||
status_code=HTTPStatus.FORBIDDEN, detail="Failed to get auth"
|
status_code=HTTPStatus.FORBIDDEN, detail="Failed to get auth"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return api_get_jukebox_device_check(juke_id, retry=True)
|
return await api_get_jukebox_device_check(juke_id, retry=True)
|
||||||
else:
|
else:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.FORBIDDEN, detail="No device connected"
|
status_code=HTTPStatus.FORBIDDEN, detail="No device connected"
|
||||||
|
|
@ -233,10 +225,8 @@ async def api_get_jukebox_device_check(
|
||||||
|
|
||||||
@jukebox_ext.get("/api/v1/jukebox/jb/invoice/{juke_id}/{song_id}")
|
@jukebox_ext.get("/api/v1/jukebox/jb/invoice/{juke_id}/{song_id}")
|
||||||
async def api_get_jukebox_invoice(juke_id, song_id):
|
async def api_get_jukebox_invoice(juke_id, song_id):
|
||||||
try:
|
|
||||||
jukebox = await get_jukebox(juke_id)
|
jukebox = await get_jukebox(juke_id)
|
||||||
|
if not jukebox:
|
||||||
except:
|
|
||||||
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="No jukebox")
|
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="No jukebox")
|
||||||
try:
|
try:
|
||||||
|
|
||||||
|
|
@ -266,8 +256,7 @@ async def api_get_jukebox_invoice(juke_id, song_id):
|
||||||
invoice=invoice[1], payment_hash=payment_hash, juke_id=juke_id, song_id=song_id
|
invoice=invoice[1], payment_hash=payment_hash, juke_id=juke_id, song_id=song_id
|
||||||
)
|
)
|
||||||
jukebox_payment = await create_jukebox_payment(data)
|
jukebox_payment = await create_jukebox_payment(data)
|
||||||
|
return jukebox_payment
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
@jukebox_ext.get("/api/v1/jukebox/jb/checkinvoice/{pay_hash}/{juke_id}")
|
@jukebox_ext.get("/api/v1/jukebox/jb/checkinvoice/{pay_hash}/{juke_id}")
|
||||||
|
|
@ -296,13 +285,12 @@ async def api_get_jukebox_invoice_paid(
|
||||||
pay_hash: str = Query(None),
|
pay_hash: str = Query(None),
|
||||||
retry: bool = Query(False),
|
retry: bool = Query(False),
|
||||||
):
|
):
|
||||||
try:
|
|
||||||
jukebox = await get_jukebox(juke_id)
|
jukebox = await get_jukebox(juke_id)
|
||||||
except:
|
if not jukebox:
|
||||||
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="No jukebox")
|
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="No jukebox")
|
||||||
await api_get_jukebox_invoice_check(pay_hash, juke_id)
|
await api_get_jukebox_invoice_check(pay_hash, juke_id)
|
||||||
jukebox_payment = await get_jukebox_payment(pay_hash)
|
jukebox_payment = await get_jukebox_payment(pay_hash)
|
||||||
if jukebox_payment.paid:
|
if jukebox_payment and jukebox_payment.paid:
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
r = await client.get(
|
r = await client.get(
|
||||||
"https://api.spotify.com/v1/me/player/currently-playing?market=ES",
|
"https://api.spotify.com/v1/me/player/currently-playing?market=ES",
|
||||||
|
|
@ -407,9 +395,8 @@ async def api_get_jukebox_invoice_paid(
|
||||||
async def api_get_jukebox_currently(
|
async def api_get_jukebox_currently(
|
||||||
retry: bool = Query(False), juke_id: str = Query(None)
|
retry: bool = Query(False), juke_id: str = Query(None)
|
||||||
):
|
):
|
||||||
try:
|
|
||||||
jukebox = await get_jukebox(juke_id)
|
jukebox = await get_jukebox(juke_id)
|
||||||
except:
|
if not jukebox:
|
||||||
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="No jukebox")
|
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="No jukebox")
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,10 @@ import json
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from lnbits.core import db as core_db
|
from lnbits.core import db as core_db
|
||||||
from lnbits.core.crud import create_payment
|
|
||||||
from lnbits.core.models import Payment
|
from lnbits.core.models import Payment
|
||||||
from lnbits.helpers import get_current_extension_name, urlsafe_short_hash
|
from lnbits.core.services import create_invoice, pay_invoice
|
||||||
from lnbits.tasks import internal_invoice_listener, register_invoice_listener
|
from lnbits.helpers import get_current_extension_name
|
||||||
|
from lnbits.tasks import register_invoice_listener
|
||||||
|
|
||||||
from .crud import get_livestream_by_track, get_producer, get_track
|
from .crud import get_livestream_by_track, get_producer, get_track
|
||||||
|
|
||||||
|
|
@ -44,44 +44,20 @@ async def on_invoice_paid(payment: Payment) -> None:
|
||||||
# now we make a special kind of internal transfer
|
# now we make a special kind of internal transfer
|
||||||
amount = int(payment.amount * (100 - ls.fee_pct) / 100)
|
amount = int(payment.amount * (100 - ls.fee_pct) / 100)
|
||||||
|
|
||||||
# mark the original payment with two extra keys, "shared_with" and "received"
|
payment_hash, payment_request = await create_invoice(
|
||||||
# (this prevents us from doing this process again and it's informative)
|
wallet_id=tpos.tip_wallet,
|
||||||
# and reduce it by the amount we're going to send to the producer
|
amount=amount, # sats
|
||||||
await core_db.execute(
|
internal=True,
|
||||||
"""
|
|
||||||
UPDATE apipayments
|
|
||||||
SET extra = ?, amount = ?
|
|
||||||
WHERE hash = ?
|
|
||||||
AND checking_id NOT LIKE 'internal_%'
|
|
||||||
""",
|
|
||||||
(
|
|
||||||
json.dumps(
|
|
||||||
dict(
|
|
||||||
**payment.extra,
|
|
||||||
shared_with=[producer.name, producer.id],
|
|
||||||
received=payment.amount,
|
|
||||||
)
|
|
||||||
),
|
|
||||||
payment.amount - amount,
|
|
||||||
payment.payment_hash,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
# perform an internal transfer using the same payment_hash to the producer wallet
|
|
||||||
internal_checking_id = f"internal_{urlsafe_short_hash()}"
|
|
||||||
await create_payment(
|
|
||||||
wallet_id=producer.wallet,
|
|
||||||
checking_id=internal_checking_id,
|
|
||||||
payment_request="",
|
|
||||||
payment_hash=payment.payment_hash,
|
|
||||||
amount=amount,
|
|
||||||
memo=f"Revenue from '{track.name}'.",
|
memo=f"Revenue from '{track.name}'.",
|
||||||
pending=False,
|
|
||||||
)
|
)
|
||||||
|
logger.debug(f"livestream: producer invoice created: {payment_hash}")
|
||||||
|
|
||||||
# manually send this for now
|
checking_id = await pay_invoice(
|
||||||
# await internal_invoice_paid.send(internal_checking_id)
|
payment_request=payment_request,
|
||||||
await internal_invoice_listener.put(internal_checking_id)
|
wallet_id=payment.wallet_id,
|
||||||
|
extra={"tag": "livestream"},
|
||||||
|
)
|
||||||
|
logger.debug(f"livestream: producer invoice paid: {checking_id}")
|
||||||
|
|
||||||
# so the flow is the following:
|
# so the flow is the following:
|
||||||
# - we receive, say, 1000 satoshis
|
# - we receive, say, 1000 satoshis
|
||||||
|
|
|
||||||
|
|
@ -60,14 +60,14 @@ async def api_update_track(track_id, g: WalletTypeInfo = Depends(get_key_type)):
|
||||||
|
|
||||||
ls = await get_or_create_livestream_by_wallet(g.wallet.id)
|
ls = await get_or_create_livestream_by_wallet(g.wallet.id)
|
||||||
await update_current_track(ls.id, id)
|
await update_current_track(ls.id, id)
|
||||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
return "", HTTPStatus.NO_CONTENT
|
||||||
|
|
||||||
|
|
||||||
@livestream_ext.put("/api/v1/livestream/fee/{fee_pct}")
|
@livestream_ext.put("/api/v1/livestream/fee/{fee_pct}")
|
||||||
async def api_update_fee(fee_pct, g: WalletTypeInfo = Depends(get_key_type)):
|
async def api_update_fee(fee_pct, g: WalletTypeInfo = Depends(get_key_type)):
|
||||||
ls = await get_or_create_livestream_by_wallet(g.wallet.id)
|
ls = await get_or_create_livestream_by_wallet(g.wallet.id)
|
||||||
await update_livestream_fee(ls.id, int(fee_pct))
|
await update_livestream_fee(ls.id, int(fee_pct))
|
||||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
return "", HTTPStatus.NO_CONTENT
|
||||||
|
|
||||||
|
|
||||||
@livestream_ext.post("/api/v1/livestream/tracks")
|
@livestream_ext.post("/api/v1/livestream/tracks")
|
||||||
|
|
@ -93,8 +93,8 @@ async def api_add_track(
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
@livestream_ext.route("/api/v1/livestream/tracks/{track_id}")
|
@livestream_ext.delete("/api/v1/livestream/tracks/{track_id}")
|
||||||
async def api_delete_track(track_id, g: WalletTypeInfo = Depends(get_key_type)):
|
async def api_delete_track(track_id, g: WalletTypeInfo = Depends(get_key_type)):
|
||||||
ls = await get_or_create_livestream_by_wallet(g.wallet.id)
|
ls = await get_or_create_livestream_by_wallet(g.wallet.id)
|
||||||
await delete_track_from_livestream(ls.id, track_id)
|
await delete_track_from_livestream(ls.id, track_id)
|
||||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
return "", HTTPStatus.NO_CONTENT
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,7 @@ async def api_domain_delete(domain_id, g: WalletTypeInfo = Depends(get_key_type)
|
||||||
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not your domain")
|
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not your domain")
|
||||||
|
|
||||||
await delete_domain(domain_id)
|
await delete_domain(domain_id)
|
||||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
return "", HTTPStatus.NO_CONTENT
|
||||||
|
|
||||||
|
|
||||||
# ADDRESSES
|
# ADDRESSES
|
||||||
|
|
@ -253,4 +253,4 @@ async def api_address_delete(address_id, g: WalletTypeInfo = Depends(get_key_typ
|
||||||
)
|
)
|
||||||
|
|
||||||
await delete_address(address_id)
|
await delete_address(address_id)
|
||||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
return "", HTTPStatus.NO_CONTENT
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,7 @@ async def api_form_delete(form_id, wallet: WalletTypeInfo = Depends(get_key_type
|
||||||
|
|
||||||
await delete_form(form_id)
|
await delete_form(form_id)
|
||||||
|
|
||||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
return "", HTTPStatus.NO_CONTENT
|
||||||
|
|
||||||
|
|
||||||
#########tickets##########
|
#########tickets##########
|
||||||
|
|
@ -160,4 +160,4 @@ async def api_ticket_delete(ticket_id, wallet: WalletTypeInfo = Depends(get_key_
|
||||||
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not your ticket.")
|
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not your ticket.")
|
||||||
|
|
||||||
await delete_ticket(ticket_id)
|
await delete_ticket(ticket_id)
|
||||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
return "", HTTPStatus.NO_CONTENT
|
||||||
|
|
|
||||||
|
|
@ -23,9 +23,22 @@ async def create_lnurldevice(
|
||||||
currency,
|
currency,
|
||||||
device,
|
device,
|
||||||
profit,
|
profit,
|
||||||
amount
|
amount,
|
||||||
|
pin,
|
||||||
|
profit1,
|
||||||
|
amount1,
|
||||||
|
pin1,
|
||||||
|
profit2,
|
||||||
|
amount2,
|
||||||
|
pin2,
|
||||||
|
profit3,
|
||||||
|
amount3,
|
||||||
|
pin3,
|
||||||
|
profit4,
|
||||||
|
amount4,
|
||||||
|
pin4
|
||||||
)
|
)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
lnurldevice_id,
|
lnurldevice_id,
|
||||||
|
|
@ -36,6 +49,19 @@ async def create_lnurldevice(
|
||||||
data.device,
|
data.device,
|
||||||
data.profit,
|
data.profit,
|
||||||
data.amount,
|
data.amount,
|
||||||
|
data.pin,
|
||||||
|
data.profit1,
|
||||||
|
data.amount1,
|
||||||
|
data.pin1,
|
||||||
|
data.profit2,
|
||||||
|
data.amount2,
|
||||||
|
data.pin2,
|
||||||
|
data.profit3,
|
||||||
|
data.amount3,
|
||||||
|
data.pin3,
|
||||||
|
data.profit4,
|
||||||
|
data.amount4,
|
||||||
|
data.pin4,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
return await get_lnurldevice(lnurldevice_id)
|
return await get_lnurldevice(lnurldevice_id)
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ from typing import Optional
|
||||||
from embit import bech32, compact
|
from embit import bech32, compact
|
||||||
from fastapi import Request
|
from fastapi import Request
|
||||||
from fastapi.param_functions import Query
|
from fastapi.param_functions import Query
|
||||||
|
from loguru import logger
|
||||||
from starlette.exceptions import HTTPException
|
from starlette.exceptions import HTTPException
|
||||||
|
|
||||||
from lnbits.core.services import create_invoice
|
from lnbits.core.services import create_invoice
|
||||||
|
|
@ -91,6 +92,9 @@ async def lnurl_v1_params(
|
||||||
device_id: str = Query(None),
|
device_id: str = Query(None),
|
||||||
p: str = Query(None),
|
p: str = Query(None),
|
||||||
atm: str = Query(None),
|
atm: str = Query(None),
|
||||||
|
gpio: str = Query(None),
|
||||||
|
profit: str = Query(None),
|
||||||
|
amount: str = Query(None),
|
||||||
):
|
):
|
||||||
device = await get_lnurldevice(device_id)
|
device = await get_lnurldevice(device_id)
|
||||||
if not device:
|
if not device:
|
||||||
|
|
@ -101,20 +105,28 @@ async def lnurl_v1_params(
|
||||||
paymentcheck = await get_lnurlpayload(p)
|
paymentcheck = await get_lnurlpayload(p)
|
||||||
if device.device == "atm":
|
if device.device == "atm":
|
||||||
if paymentcheck:
|
if paymentcheck:
|
||||||
|
if paymentcheck.payhash != "payment_hash":
|
||||||
return {"status": "ERROR", "reason": f"Payment already claimed"}
|
return {"status": "ERROR", "reason": f"Payment already claimed"}
|
||||||
if device.device == "switch":
|
if device.device == "switch":
|
||||||
|
|
||||||
price_msat = (
|
price_msat = (
|
||||||
await fiat_amount_as_satoshis(float(device.profit), device.currency)
|
await fiat_amount_as_satoshis(float(profit), device.currency)
|
||||||
if device.currency != "sat"
|
if device.currency != "sat"
|
||||||
else amount_in_cent
|
else amount_in_cent
|
||||||
) * 1000
|
) * 1000
|
||||||
|
|
||||||
|
# Check they're not trying to trick the switch!
|
||||||
|
check = False
|
||||||
|
for switch in device.switches(request):
|
||||||
|
if switch[0] == gpio and switch[1] == profit and switch[2] == amount:
|
||||||
|
check = True
|
||||||
|
if not check:
|
||||||
|
return {"status": "ERROR", "reason": f"Switch params wrong"}
|
||||||
|
|
||||||
lnurldevicepayment = await create_lnurldevicepayment(
|
lnurldevicepayment = await create_lnurldevicepayment(
|
||||||
deviceid=device.id,
|
deviceid=device.id,
|
||||||
payload="bla",
|
payload=amount,
|
||||||
sats=price_msat,
|
sats=price_msat,
|
||||||
pin=1,
|
pin=gpio,
|
||||||
payhash="bla",
|
payhash="bla",
|
||||||
)
|
)
|
||||||
if not lnurldevicepayment:
|
if not lnurldevicepayment:
|
||||||
|
|
@ -126,7 +138,7 @@ async def lnurl_v1_params(
|
||||||
),
|
),
|
||||||
"minSendable": price_msat,
|
"minSendable": price_msat,
|
||||||
"maxSendable": price_msat,
|
"maxSendable": price_msat,
|
||||||
"metadata": await device.lnurlpay_metadata(),
|
"metadata": device.lnurlpay_metadata,
|
||||||
}
|
}
|
||||||
if len(p) % 4 > 0:
|
if len(p) % 4 > 0:
|
||||||
p += "=" * (4 - (len(p) % 4))
|
p += "=" * (4 - (len(p) % 4))
|
||||||
|
|
@ -165,7 +177,7 @@ async def lnurl_v1_params(
|
||||||
"callback": request.url_for(
|
"callback": request.url_for(
|
||||||
"lnurldevice.lnurl_callback", paymentid=lnurldevicepayment.id
|
"lnurldevice.lnurl_callback", paymentid=lnurldevicepayment.id
|
||||||
),
|
),
|
||||||
"k1": lnurldevicepayment.id,
|
"k1": p,
|
||||||
"minWithdrawable": price_msat * 1000,
|
"minWithdrawable": price_msat * 1000,
|
||||||
"maxWithdrawable": price_msat * 1000,
|
"maxWithdrawable": price_msat * 1000,
|
||||||
"defaultDescription": device.title,
|
"defaultDescription": device.title,
|
||||||
|
|
@ -188,7 +200,7 @@ async def lnurl_v1_params(
|
||||||
),
|
),
|
||||||
"minSendable": price_msat * 1000,
|
"minSendable": price_msat * 1000,
|
||||||
"maxSendable": price_msat * 1000,
|
"maxSendable": price_msat * 1000,
|
||||||
"metadata": await device.lnurlpay_metadata(),
|
"metadata": device.lnurlpay_metadata,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -215,14 +227,13 @@ async def lnurl_callback(
|
||||||
status_code=HTTPStatus.FORBIDDEN, detail="No payment request"
|
status_code=HTTPStatus.FORBIDDEN, detail="No payment request"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
if lnurldevicepayment.id != k1:
|
if lnurldevicepayment.payload != k1:
|
||||||
return {"status": "ERROR", "reason": "Bad K1"}
|
return {"status": "ERROR", "reason": "Bad K1"}
|
||||||
if lnurldevicepayment.payhash != "payment_hash":
|
if lnurldevicepayment.payhash != "payment_hash":
|
||||||
return {"status": "ERROR", "reason": f"Payment already claimed"}
|
return {"status": "ERROR", "reason": f"Payment already claimed"}
|
||||||
lnurldevicepayment = await update_lnurldevicepayment(
|
lnurldevicepayment = await update_lnurldevicepayment(
|
||||||
lnurldevicepayment_id=paymentid, payhash=lnurldevicepayment.payload
|
lnurldevicepayment_id=paymentid, payhash=lnurldevicepayment.payload
|
||||||
)
|
)
|
||||||
|
|
||||||
await pay_invoice(
|
await pay_invoice(
|
||||||
wallet_id=device.wallet,
|
wallet_id=device.wallet,
|
||||||
payment_request=pr,
|
payment_request=pr,
|
||||||
|
|
@ -233,11 +244,17 @@ async def lnurl_callback(
|
||||||
if device.device == "switch":
|
if device.device == "switch":
|
||||||
payment_hash, payment_request = await create_invoice(
|
payment_hash, payment_request = await create_invoice(
|
||||||
wallet_id=device.wallet,
|
wallet_id=device.wallet,
|
||||||
amount=lnurldevicepayment.sats / 1000,
|
amount=int(lnurldevicepayment.sats / 1000),
|
||||||
memo=device.title + "-" + lnurldevicepayment.id,
|
memo=device.id + " PIN " + str(lnurldevicepayment.pin),
|
||||||
unhashed_description=(await device.lnurlpay_metadata()).encode("utf-8"),
|
unhashed_description=device.lnurlpay_metadata.encode("utf-8"),
|
||||||
extra={"tag": "Switch", "id": paymentid, "time": device.amount},
|
extra={
|
||||||
|
"tag": "Switch",
|
||||||
|
"pin": str(lnurldevicepayment.pin),
|
||||||
|
"amount": str(lnurldevicepayment.payload),
|
||||||
|
"id": paymentid,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
lnurldevicepayment = await update_lnurldevicepayment(
|
lnurldevicepayment = await update_lnurldevicepayment(
|
||||||
lnurldevicepayment_id=paymentid, payhash=payment_hash
|
lnurldevicepayment_id=paymentid, payhash=payment_hash
|
||||||
)
|
)
|
||||||
|
|
@ -248,9 +265,9 @@ async def lnurl_callback(
|
||||||
|
|
||||||
payment_hash, payment_request = await create_invoice(
|
payment_hash, payment_request = await create_invoice(
|
||||||
wallet_id=device.wallet,
|
wallet_id=device.wallet,
|
||||||
amount=lnurldevicepayment.sats / 1000,
|
amount=int(lnurldevicepayment.sats / 1000),
|
||||||
memo=device.title,
|
memo=device.title,
|
||||||
unhashed_description=(await device.lnurlpay_metadata()).encode("utf-8"),
|
unhashed_description=device.lnurlpay_metadata.encode("utf-8"),
|
||||||
extra={"tag": "PoS"},
|
extra={"tag": "PoS"},
|
||||||
)
|
)
|
||||||
lnurldevicepayment = await update_lnurldevicepayment(
|
lnurldevicepayment = await update_lnurldevicepayment(
|
||||||
|
|
|
||||||
|
|
@ -88,3 +88,52 @@ async def m003_redux(db):
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"ALTER TABLE lnurldevice.lnurldevices ADD COLUMN amount INT DEFAULT 0;"
|
"ALTER TABLE lnurldevice.lnurldevices ADD COLUMN amount INT DEFAULT 0;"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def m004_redux(db):
|
||||||
|
"""
|
||||||
|
Add 'meta' for storing various metadata about the wallet
|
||||||
|
"""
|
||||||
|
await db.execute(
|
||||||
|
"ALTER TABLE lnurldevice.lnurldevices ADD COLUMN pin INT DEFAULT 0"
|
||||||
|
)
|
||||||
|
|
||||||
|
await db.execute(
|
||||||
|
"ALTER TABLE lnurldevice.lnurldevices ADD COLUMN profit1 FLOAT DEFAULT 0"
|
||||||
|
)
|
||||||
|
await db.execute(
|
||||||
|
"ALTER TABLE lnurldevice.lnurldevices ADD COLUMN amount1 INT DEFAULT 0"
|
||||||
|
)
|
||||||
|
await db.execute(
|
||||||
|
"ALTER TABLE lnurldevice.lnurldevices ADD COLUMN pin1 INT DEFAULT 0"
|
||||||
|
)
|
||||||
|
|
||||||
|
await db.execute(
|
||||||
|
"ALTER TABLE lnurldevice.lnurldevices ADD COLUMN profit2 FLOAT DEFAULT 0"
|
||||||
|
)
|
||||||
|
await db.execute(
|
||||||
|
"ALTER TABLE lnurldevice.lnurldevices ADD COLUMN amount2 INT DEFAULT 0"
|
||||||
|
)
|
||||||
|
await db.execute(
|
||||||
|
"ALTER TABLE lnurldevice.lnurldevices ADD COLUMN pin2 INT DEFAULT 0"
|
||||||
|
)
|
||||||
|
|
||||||
|
await db.execute(
|
||||||
|
"ALTER TABLE lnurldevice.lnurldevices ADD COLUMN profit3 FLOAT DEFAULT 0"
|
||||||
|
)
|
||||||
|
await db.execute(
|
||||||
|
"ALTER TABLE lnurldevice.lnurldevices ADD COLUMN amount3 INT DEFAULT 0"
|
||||||
|
)
|
||||||
|
await db.execute(
|
||||||
|
"ALTER TABLE lnurldevice.lnurldevices ADD COLUMN pin3 INT DEFAULT 0"
|
||||||
|
)
|
||||||
|
|
||||||
|
await db.execute(
|
||||||
|
"ALTER TABLE lnurldevice.lnurldevices ADD COLUMN profit4 FLOAT DEFAULT 0"
|
||||||
|
)
|
||||||
|
await db.execute(
|
||||||
|
"ALTER TABLE lnurldevice.lnurldevices ADD COLUMN amount4 INT DEFAULT 0"
|
||||||
|
)
|
||||||
|
await db.execute(
|
||||||
|
"ALTER TABLE lnurldevice.lnurldevices ADD COLUMN pin4 INT DEFAULT 0"
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
import json
|
import json
|
||||||
from sqlite3 import Row
|
from sqlite3 import Row
|
||||||
from typing import Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
from fastapi import Request
|
from fastapi import Request
|
||||||
from lnurl import Lnurl
|
from lnurl import Lnurl
|
||||||
from lnurl import encode as lnurl_encode # type: ignore
|
from lnurl import encode as lnurl_encode # type: ignore
|
||||||
from lnurl.models import LnurlPaySuccessAction, UrlAction # type: ignore
|
from lnurl.models import LnurlPaySuccessAction, UrlAction # type: ignore
|
||||||
from lnurl.types import LnurlPayMetadata # type: ignore
|
from lnurl.types import LnurlPayMetadata # type: ignore
|
||||||
|
from loguru import logger
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from pydantic.main import BaseModel
|
from pydantic.main import BaseModel
|
||||||
|
|
||||||
|
|
@ -18,6 +19,19 @@ class createLnurldevice(BaseModel):
|
||||||
device: str
|
device: str
|
||||||
profit: float
|
profit: float
|
||||||
amount: int
|
amount: int
|
||||||
|
pin: int = 0
|
||||||
|
profit1: float = 0
|
||||||
|
amount1: int = 0
|
||||||
|
pin1: int = 0
|
||||||
|
profit2: float = 0
|
||||||
|
amount2: int = 0
|
||||||
|
pin2: int = 0
|
||||||
|
profit3: float = 0
|
||||||
|
amount3: int = 0
|
||||||
|
pin3: int = 0
|
||||||
|
profit4: float = 0
|
||||||
|
amount4: int = 0
|
||||||
|
pin4: int = 0
|
||||||
|
|
||||||
|
|
||||||
class lnurldevices(BaseModel):
|
class lnurldevices(BaseModel):
|
||||||
|
|
@ -29,18 +43,122 @@ class lnurldevices(BaseModel):
|
||||||
device: str
|
device: str
|
||||||
profit: float
|
profit: float
|
||||||
amount: int
|
amount: int
|
||||||
|
pin: int
|
||||||
|
profit1: float
|
||||||
|
amount1: int
|
||||||
|
pin1: int
|
||||||
|
profit2: float
|
||||||
|
amount2: int
|
||||||
|
pin2: int
|
||||||
|
profit3: float
|
||||||
|
amount3: int
|
||||||
|
pin3: int
|
||||||
|
profit4: float
|
||||||
|
amount4: int
|
||||||
|
pin4: int
|
||||||
timestamp: str
|
timestamp: str
|
||||||
|
|
||||||
def from_row(cls, row: Row) -> "lnurldevices":
|
def from_row(cls, row: Row) -> "lnurldevices":
|
||||||
return cls(**dict(row))
|
return cls(**dict(row))
|
||||||
|
|
||||||
def lnurl(self, req: Request) -> Lnurl:
|
@property
|
||||||
url = req.url_for("lnurldevice.lnurl_v1_params", device_id=self.id)
|
def lnurlpay_metadata(self) -> LnurlPayMetadata:
|
||||||
return lnurl_encode(url)
|
|
||||||
|
|
||||||
async def lnurlpay_metadata(self) -> LnurlPayMetadata:
|
|
||||||
return LnurlPayMetadata(json.dumps([["text/plain", self.title]]))
|
return LnurlPayMetadata(json.dumps([["text/plain", self.title]]))
|
||||||
|
|
||||||
|
def switches(self, req: Request) -> List:
|
||||||
|
switches = []
|
||||||
|
if self.profit > 0:
|
||||||
|
url = req.url_for("lnurldevice.lnurl_v1_params", device_id=self.id)
|
||||||
|
switches.append(
|
||||||
|
[
|
||||||
|
str(self.pin),
|
||||||
|
str(self.profit),
|
||||||
|
str(self.amount),
|
||||||
|
lnurl_encode(
|
||||||
|
url
|
||||||
|
+ "?gpio="
|
||||||
|
+ str(self.pin)
|
||||||
|
+ "&profit="
|
||||||
|
+ str(self.profit)
|
||||||
|
+ "&amount="
|
||||||
|
+ str(self.amount)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
if self.profit1 > 0:
|
||||||
|
url = req.url_for("lnurldevice.lnurl_v1_params", device_id=self.id)
|
||||||
|
switches.append(
|
||||||
|
[
|
||||||
|
str(self.pin1),
|
||||||
|
str(self.profit1),
|
||||||
|
str(self.amount1),
|
||||||
|
lnurl_encode(
|
||||||
|
url
|
||||||
|
+ "?gpio="
|
||||||
|
+ str(self.pin1)
|
||||||
|
+ "&profit="
|
||||||
|
+ str(self.profit1)
|
||||||
|
+ "&amount="
|
||||||
|
+ str(self.amount1)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
if self.profit2 > 0:
|
||||||
|
url = req.url_for("lnurldevice.lnurl_v1_params", device_id=self.id)
|
||||||
|
switches.append(
|
||||||
|
[
|
||||||
|
str(self.pin2),
|
||||||
|
str(self.profit2),
|
||||||
|
str(self.amount2),
|
||||||
|
lnurl_encode(
|
||||||
|
url
|
||||||
|
+ "?gpio="
|
||||||
|
+ str(self.pin2)
|
||||||
|
+ "&profit="
|
||||||
|
+ str(self.profit2)
|
||||||
|
+ "&amount="
|
||||||
|
+ str(self.amount2)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
if self.profit3 > 0:
|
||||||
|
url = req.url_for("lnurldevice.lnurl_v1_params", device_id=self.id)
|
||||||
|
switches.append(
|
||||||
|
[
|
||||||
|
str(self.pin3),
|
||||||
|
str(self.profit3),
|
||||||
|
str(self.amount3),
|
||||||
|
lnurl_encode(
|
||||||
|
url
|
||||||
|
+ "?gpio="
|
||||||
|
+ str(self.pin3)
|
||||||
|
+ "&profit="
|
||||||
|
+ str(self.profit3)
|
||||||
|
+ "&amount="
|
||||||
|
+ str(self.amount3)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
if self.profit4 > 0:
|
||||||
|
url = req.url_for("lnurldevice.lnurl_v1_params", device_id=self.id)
|
||||||
|
switches.append(
|
||||||
|
[
|
||||||
|
str(self.pin4),
|
||||||
|
str(self.profit4),
|
||||||
|
str(self.amount4),
|
||||||
|
lnurl_encode(
|
||||||
|
url
|
||||||
|
+ "?gpio="
|
||||||
|
+ str(self.pin4)
|
||||||
|
+ "&profit="
|
||||||
|
+ str(self.profit4)
|
||||||
|
+ "&amount="
|
||||||
|
+ str(self.amount4)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
return switches
|
||||||
|
|
||||||
|
|
||||||
class lnurldevicepayment(BaseModel):
|
class lnurldevicepayment(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
|
|
|
||||||
|
|
@ -36,5 +36,9 @@ async def on_invoice_paid(payment: Payment) -> None:
|
||||||
lnurldevicepayment = await update_lnurldevicepayment(
|
lnurldevicepayment = await update_lnurldevicepayment(
|
||||||
lnurldevicepayment_id=payment.extra.get("id"), payhash="used"
|
lnurldevicepayment_id=payment.extra.get("id"), payhash="used"
|
||||||
)
|
)
|
||||||
return await updater(lnurldevicepayment.deviceid)
|
return await updater(
|
||||||
|
lnurldevicepayment.deviceid,
|
||||||
|
lnurldevicepayment.pin,
|
||||||
|
lnurldevicepayment.payload,
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -105,7 +105,7 @@
|
||||||
@click="openQrCodeDialog(props.row.id)"
|
@click="openQrCodeDialog(props.row.id)"
|
||||||
><q-tooltip v-if="protocol == 'http:'">
|
><q-tooltip v-if="protocol == 'http:'">
|
||||||
LNURLs only work over HTTPS </q-tooltip
|
LNURLs only work over HTTPS </q-tooltip
|
||||||
><q-tooltip v-else> view LNURL </q-tooltip></q-btn
|
><q-tooltip v-else> view LNURLS </q-tooltip></q-btn
|
||||||
>
|
>
|
||||||
</q-td>
|
</q-td>
|
||||||
<q-td
|
<q-td
|
||||||
|
|
@ -230,6 +230,30 @@
|
||||||
label="Profit margin (% added to invoices/deducted from faucets)"
|
label="Profit margin (% added to invoices/deducted from faucets)"
|
||||||
></q-input>
|
></q-input>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
|
<q-btn
|
||||||
|
unelevated
|
||||||
|
class="q-mb-lg"
|
||||||
|
round
|
||||||
|
size="sm"
|
||||||
|
icon="add"
|
||||||
|
@click="addSwitch"
|
||||||
|
v-model="switches"
|
||||||
|
color="primary"
|
||||||
|
></q-btn>
|
||||||
|
<q-btn
|
||||||
|
unelevated
|
||||||
|
class="q-mb-lg"
|
||||||
|
round
|
||||||
|
size="sm"
|
||||||
|
icon="remove"
|
||||||
|
@click="removeSwitch"
|
||||||
|
v-model="switches"
|
||||||
|
color="primary"
|
||||||
|
></q-btn>
|
||||||
|
|
||||||
|
<div v-if="switches >= 0">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
<q-input
|
<q-input
|
||||||
ref="setAmount"
|
ref="setAmount"
|
||||||
filled
|
filled
|
||||||
|
|
@ -243,6 +267,8 @@
|
||||||
:step="'0.01'"
|
:step="'0.01'"
|
||||||
value="0.00"
|
value="0.00"
|
||||||
></q-input>
|
></q-input>
|
||||||
|
</div>
|
||||||
|
<div class="col q-ml-md">
|
||||||
<q-input
|
<q-input
|
||||||
filled
|
filled
|
||||||
dense
|
dense
|
||||||
|
|
@ -252,7 +278,173 @@
|
||||||
label="milesecs to turn Switch on for (1sec = 1000ms)"
|
label="milesecs to turn Switch on for (1sec = 1000ms)"
|
||||||
></q-input>
|
></q-input>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col q-ml-md">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="formDialoglnurldevice.data.pin"
|
||||||
|
type="number"
|
||||||
|
label="GPIO to turn on"
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="switches >= 1">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<q-input
|
||||||
|
ref="setAmount"
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="formDialoglnurldevice.data.profit1"
|
||||||
|
class="q-pb-md"
|
||||||
|
:label="'Amount (' + formDialoglnurldevice.data.currency + ') *'"
|
||||||
|
:mask="'#.##'"
|
||||||
|
fill-mask="0"
|
||||||
|
reverse-fill-mask
|
||||||
|
:step="'0.01'"
|
||||||
|
value="0.00"
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
<div class="col q-ml-md">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="formDialoglnurldevice.data.amount1"
|
||||||
|
type="number"
|
||||||
|
value="1000"
|
||||||
|
label="milesecs to turn Switch on for (1sec = 1000ms)"
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
<div class="col q-ml-md">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="formDialoglnurldevice.data.pin1"
|
||||||
|
type="number"
|
||||||
|
label="GPIO to turn on"
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="switches >= 2">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<q-input
|
||||||
|
ref="setAmount"
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="formDialoglnurldevice.data.profit2"
|
||||||
|
class="q-pb-md"
|
||||||
|
:label="'Amount (' + formDialoglnurldevice.data.currency + ') *'"
|
||||||
|
:mask="'#.##'"
|
||||||
|
fill-mask="0"
|
||||||
|
reverse-fill-mask
|
||||||
|
:step="'0.01'"
|
||||||
|
value="0.00"
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
<div class="col q-ml-md">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="formDialoglnurldevice.data.amount2"
|
||||||
|
type="number"
|
||||||
|
value="1000"
|
||||||
|
label="milesecs to turn Switch on for (1sec = 1000ms)"
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
<div class="col q-ml-md">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="formDialoglnurldevice.data.pin2"
|
||||||
|
type="number"
|
||||||
|
label="GPIO to turn on"
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="switches >= 3">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<q-input
|
||||||
|
ref="setAmount"
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="formDialoglnurldevice.data.profit3"
|
||||||
|
class="q-pb-md"
|
||||||
|
:label="'Amount (' + formDialoglnurldevice.data.currency + ') *'"
|
||||||
|
:mask="'#.##'"
|
||||||
|
fill-mask="0"
|
||||||
|
reverse-fill-mask
|
||||||
|
:step="'0.01'"
|
||||||
|
value="0.00"
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
<div class="col q-ml-md">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="formDialoglnurldevice.data.amount3"
|
||||||
|
type="number"
|
||||||
|
value="1000"
|
||||||
|
label="milesecs to turn Switch on for (1sec = 1000ms)"
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
<div class="col q-ml-md">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="formDialoglnurldevice.data.pin3"
|
||||||
|
type="number"
|
||||||
|
label="GPIO to turn on"
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="switches >= 4">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<q-input
|
||||||
|
ref="setAmount"
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="formDialoglnurldevice.data.profit4"
|
||||||
|
class="q-pb-md"
|
||||||
|
:label="'Amount (' + formDialoglnurldevice.data.currency + ') *'"
|
||||||
|
:mask="'#.##'"
|
||||||
|
fill-mask="0"
|
||||||
|
reverse-fill-mask
|
||||||
|
:step="'0.01'"
|
||||||
|
value="0.00"
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
<div class="col q-ml-md">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="formDialoglnurldevice.data.amount4"
|
||||||
|
type="number"
|
||||||
|
value="1000"
|
||||||
|
label="milesecs to turn Switch on for (1sec = 1000ms)"
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
<div class="col q-ml-md">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="formDialoglnurldevice.data.pin4"
|
||||||
|
type="number"
|
||||||
|
label="GPIO to turn on"
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="row q-mt-lg">
|
<div class="row q-mt-lg">
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="formDialoglnurldevice.data.id"
|
v-if="formDialoglnurldevice.data.id"
|
||||||
|
|
@ -284,24 +476,26 @@
|
||||||
<q-card v-if="qrCodeDialog.data" class="q-pa-lg lnbits__dialog-card">
|
<q-card v-if="qrCodeDialog.data" class="q-pa-lg lnbits__dialog-card">
|
||||||
<q-responsive :ratio="1" class="q-mx-xl q-mb-md">
|
<q-responsive :ratio="1" class="q-mx-xl q-mb-md">
|
||||||
<qrcode
|
<qrcode
|
||||||
:value="qrCodeDialog.data.url + '/?lightning=' + qrCodeDialog.data.lnurl"
|
:value="lnurlValue"
|
||||||
:options="{width: 800}"
|
:options="{width: 800}"
|
||||||
class="rounded-borders"
|
class="rounded-borders"
|
||||||
></qrcode>
|
></qrcode>
|
||||||
{% raw %}
|
|
||||||
</q-responsive>
|
</q-responsive>
|
||||||
<p style="word-break: break-all">
|
|
||||||
<strong>ID:</strong> {{ qrCodeDialog.data.id }}<br />
|
|
||||||
</p>
|
|
||||||
{% endraw %}
|
|
||||||
<div class="row q-mt-lg q-gutter-sm">
|
|
||||||
<q-btn
|
<q-btn
|
||||||
outline
|
outline
|
||||||
color="grey"
|
color="grey"
|
||||||
@click="copyText(qrCodeDialog.data.lnurl, 'LNURL copied to clipboard!')"
|
@click="copyText(lnurlValue, 'LNURL copied to clipboard!')"
|
||||||
class="q-ml-sm"
|
|
||||||
>Copy LNURL</q-btn
|
>Copy LNURL</q-btn
|
||||||
>
|
>
|
||||||
|
<br />
|
||||||
|
<div class="row q-mt-lg q-gutter-sm">
|
||||||
|
<q-btn
|
||||||
|
v-for="switch_ in qrCodeDialog.data.switches"
|
||||||
|
outline
|
||||||
|
color="primary"
|
||||||
|
:label="'Switch PIN:' + switch_[0]"
|
||||||
|
@click="lnurlValueFetch(switch_[3])"
|
||||||
|
></q-btn>
|
||||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
|
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
|
||||||
</div>
|
</div>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
|
@ -333,11 +527,14 @@
|
||||||
mixins: [windowMixin],
|
mixins: [windowMixin],
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
|
tab: 'mails',
|
||||||
protocol: window.location.protocol,
|
protocol: window.location.protocol,
|
||||||
location: window.location.hostname,
|
location: window.location.hostname,
|
||||||
wslocation: window.location.hostname,
|
wslocation: window.location.hostname,
|
||||||
filter: '',
|
filter: '',
|
||||||
currency: 'USD',
|
currency: 'USD',
|
||||||
|
lnurlValue: '',
|
||||||
|
switches: 0,
|
||||||
lnurldeviceLinks: [],
|
lnurldeviceLinks: [],
|
||||||
lnurldeviceLinksObj: [],
|
lnurldeviceLinksObj: [],
|
||||||
devices: [
|
devices: [
|
||||||
|
|
@ -386,12 +583,6 @@
|
||||||
label: 'device',
|
label: 'device',
|
||||||
field: 'device'
|
field: 'device'
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'profit',
|
|
||||||
align: 'left',
|
|
||||||
label: 'profit',
|
|
||||||
field: 'profit'
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'currency',
|
name: 'currency',
|
||||||
align: 'left',
|
align: 'left',
|
||||||
|
|
@ -440,8 +631,20 @@
|
||||||
this.qrCodeDialog.data = _.clone(lnurldevice)
|
this.qrCodeDialog.data = _.clone(lnurldevice)
|
||||||
this.qrCodeDialog.data.url =
|
this.qrCodeDialog.data.url =
|
||||||
window.location.protocol + '//' + window.location.host
|
window.location.protocol + '//' + window.location.host
|
||||||
|
this.lnurlValueFetch(this.qrCodeDialog.data.switches[0][3])
|
||||||
this.qrCodeDialog.show = true
|
this.qrCodeDialog.show = true
|
||||||
},
|
},
|
||||||
|
lnurlValueFetch: function (lnurl) {
|
||||||
|
this.lnurlValue = lnurl
|
||||||
|
},
|
||||||
|
addSwitch: function () {
|
||||||
|
var self = this
|
||||||
|
self.switches = self.switches + 1
|
||||||
|
},
|
||||||
|
removeSwitch: function () {
|
||||||
|
var self = this
|
||||||
|
self.switches = self.switches - 1
|
||||||
|
},
|
||||||
cancellnurldevice: function (data) {
|
cancellnurldevice: function (data) {
|
||||||
var self = this
|
var self = this
|
||||||
self.formDialoglnurldevice.show = false
|
self.formDialoglnurldevice.show = false
|
||||||
|
|
@ -498,7 +701,9 @@
|
||||||
.then(function (response) {
|
.then(function (response) {
|
||||||
if (response.data) {
|
if (response.data) {
|
||||||
self.lnurldeviceLinks = response.data.map(maplnurldevice)
|
self.lnurldeviceLinks = response.data.map(maplnurldevice)
|
||||||
|
console.log('response.data')
|
||||||
console.log(response.data)
|
console.log(response.data)
|
||||||
|
console.log('response.data')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(function (error) {
|
.catch(function (error) {
|
||||||
|
|
|
||||||
|
|
@ -103,8 +103,10 @@ async def websocket_endpoint(websocket: WebSocket, lnurldevice_id: str):
|
||||||
manager.disconnect(websocket)
|
manager.disconnect(websocket)
|
||||||
|
|
||||||
|
|
||||||
async def updater(lnurldevice_id):
|
async def updater(lnurldevice_id, lnurldevice_pin, lnurldevice_amount):
|
||||||
lnurldevice = await get_lnurldevice(lnurldevice_id)
|
lnurldevice = await get_lnurldevice(lnurldevice_id)
|
||||||
if not lnurldevice:
|
if not lnurldevice:
|
||||||
return
|
return
|
||||||
await manager.send_personal_message(f"{lnurldevice.amount}", lnurldevice_id)
|
return await manager.send_personal_message(
|
||||||
|
f"{lnurldevice_pin}-{lnurldevice_amount}", lnurldevice_id
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -39,10 +39,10 @@ async def api_lnurldevice_create_or_update(
|
||||||
):
|
):
|
||||||
if not lnurldevice_id:
|
if not lnurldevice_id:
|
||||||
lnurldevice = await create_lnurldevice(data)
|
lnurldevice = await create_lnurldevice(data)
|
||||||
return {**lnurldevice.dict(), **{"lnurl": lnurldevice.lnurl(req)}}
|
return {**lnurldevice.dict(), **{"switches": lnurldevice.switches(req)}}
|
||||||
else:
|
else:
|
||||||
lnurldevice = await update_lnurldevice(data, lnurldevice_id=lnurldevice_id)
|
lnurldevice = await update_lnurldevice(data, lnurldevice_id=lnurldevice_id)
|
||||||
return {**lnurldevice.dict(), **{"lnurl": lnurldevice.lnurl(req)}}
|
return {**lnurldevice.dict(), **{"switches": lnurldevice.switches(req)}}
|
||||||
|
|
||||||
|
|
||||||
@lnurldevice_ext.get("/api/v1/lnurlpos")
|
@lnurldevice_ext.get("/api/v1/lnurlpos")
|
||||||
|
|
@ -52,7 +52,7 @@ async def api_lnurldevices_retrieve(
|
||||||
wallet_ids = (await get_user(wallet.wallet.user)).wallet_ids
|
wallet_ids = (await get_user(wallet.wallet.user)).wallet_ids
|
||||||
try:
|
try:
|
||||||
return [
|
return [
|
||||||
{**lnurldevice.dict(), **{"lnurl": lnurldevice.lnurl(req)}}
|
{**lnurldevice.dict(), **{"switches": lnurldevice.switches(req)}}
|
||||||
for lnurldevice in await get_lnurldevices(wallet_ids)
|
for lnurldevice in await get_lnurldevices(wallet_ids)
|
||||||
]
|
]
|
||||||
except:
|
except:
|
||||||
|
|
@ -78,7 +78,7 @@ async def api_lnurldevice_retrieve(
|
||||||
)
|
)
|
||||||
if not lnurldevice.lnurl_toggle:
|
if not lnurldevice.lnurl_toggle:
|
||||||
return {**lnurldevice.dict()}
|
return {**lnurldevice.dict()}
|
||||||
return {**lnurldevice.dict(), **{"lnurl": lnurldevice.lnurl(req)}}
|
return {**lnurldevice.dict(), **{"switches": lnurldevice.switches(req)}}
|
||||||
|
|
||||||
|
|
||||||
@lnurldevice_ext.delete("/api/v1/lnurlpos/{lnurldevice_id}")
|
@lnurldevice_ext.delete("/api/v1/lnurlpos/{lnurldevice_id}")
|
||||||
|
|
|
||||||
|
|
@ -21,13 +21,15 @@ async def create_pay_link(data: CreatePayLinkData, wallet_id: str) -> PayLink:
|
||||||
served_meta,
|
served_meta,
|
||||||
served_pr,
|
served_pr,
|
||||||
webhook_url,
|
webhook_url,
|
||||||
|
webhook_headers,
|
||||||
|
webhook_body,
|
||||||
success_text,
|
success_text,
|
||||||
success_url,
|
success_url,
|
||||||
comment_chars,
|
comment_chars,
|
||||||
currency,
|
currency,
|
||||||
fiat_base_multiplier
|
fiat_base_multiplier
|
||||||
)
|
)
|
||||||
VALUES (?, ?, ?, ?, 0, 0, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, 0, 0, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
{returning}
|
{returning}
|
||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
|
|
@ -36,6 +38,8 @@ async def create_pay_link(data: CreatePayLinkData, wallet_id: str) -> PayLink:
|
||||||
data.min,
|
data.min,
|
||||||
data.max,
|
data.max,
|
||||||
data.webhook_url,
|
data.webhook_url,
|
||||||
|
data.webhook_headers,
|
||||||
|
data.webhook_body,
|
||||||
data.success_text,
|
data.success_text,
|
||||||
data.success_url,
|
data.success_url,
|
||||||
data.comment_chars,
|
data.comment_chars,
|
||||||
|
|
|
||||||
|
|
@ -60,3 +60,11 @@ async def m004_fiat_base_multiplier(db):
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"ALTER TABLE lnurlp.pay_links ADD COLUMN fiat_base_multiplier INTEGER DEFAULT 1;"
|
"ALTER TABLE lnurlp.pay_links ADD COLUMN fiat_base_multiplier INTEGER DEFAULT 1;"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def m005_webhook_headers_and_body(db):
|
||||||
|
"""
|
||||||
|
Add headers and body to webhooks
|
||||||
|
"""
|
||||||
|
await db.execute("ALTER TABLE lnurlp.pay_links ADD COLUMN webhook_headers TEXT;")
|
||||||
|
await db.execute("ALTER TABLE lnurlp.pay_links ADD COLUMN webhook_body TEXT;")
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,8 @@ class CreatePayLinkData(BaseModel):
|
||||||
currency: str = Query(None)
|
currency: str = Query(None)
|
||||||
comment_chars: int = Query(0, ge=0, lt=800)
|
comment_chars: int = Query(0, ge=0, lt=800)
|
||||||
webhook_url: str = Query(None)
|
webhook_url: str = Query(None)
|
||||||
|
webhook_headers: str = Query(None)
|
||||||
|
webhook_body: str = Query(None)
|
||||||
success_text: str = Query(None)
|
success_text: str = Query(None)
|
||||||
success_url: str = Query(None)
|
success_url: str = Query(None)
|
||||||
fiat_base_multiplier: int = Query(100, ge=1)
|
fiat_base_multiplier: int = Query(100, ge=1)
|
||||||
|
|
@ -31,6 +33,8 @@ class PayLink(BaseModel):
|
||||||
served_meta: int
|
served_meta: int
|
||||||
served_pr: int
|
served_pr: int
|
||||||
webhook_url: Optional[str]
|
webhook_url: Optional[str]
|
||||||
|
webhook_headers: Optional[str]
|
||||||
|
webhook_body: Optional[str]
|
||||||
success_text: Optional[str]
|
success_text: Optional[str]
|
||||||
success_url: Optional[str]
|
success_url: Optional[str]
|
||||||
currency: Optional[str]
|
currency: Optional[str]
|
||||||
|
|
|
||||||
|
|
@ -33,17 +33,22 @@ async def on_invoice_paid(payment: Payment) -> None:
|
||||||
if pay_link and pay_link.webhook_url:
|
if pay_link and pay_link.webhook_url:
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
try:
|
try:
|
||||||
r = await client.post(
|
kwargs = {
|
||||||
pay_link.webhook_url,
|
"json": {
|
||||||
json={
|
|
||||||
"payment_hash": payment.payment_hash,
|
"payment_hash": payment.payment_hash,
|
||||||
"payment_request": payment.bolt11,
|
"payment_request": payment.bolt11,
|
||||||
"amount": payment.amount,
|
"amount": payment.amount,
|
||||||
"comment": payment.extra.get("comment"),
|
"comment": payment.extra.get("comment"),
|
||||||
"lnurlp": pay_link.id,
|
"lnurlp": pay_link.id,
|
||||||
},
|
},
|
||||||
timeout=40,
|
"timeout": 40,
|
||||||
)
|
}
|
||||||
|
if pay_link.webhook_body:
|
||||||
|
kwargs["json"]["body"] = json.loads(pay_link.webhook_body)
|
||||||
|
if pay_link.webhook_headers:
|
||||||
|
kwargs["headers"] = json.loads(pay_link.webhook_headers)
|
||||||
|
|
||||||
|
r = await client.post(pay_link.webhook_url, **kwargs)
|
||||||
await mark_webhook_sent(payment, r.status_code)
|
await mark_webhook_sent(payment, r.status_code)
|
||||||
except (httpx.ConnectError, httpx.RequestError):
|
except (httpx.ConnectError, httpx.RequestError):
|
||||||
await mark_webhook_sent(payment, -1)
|
await mark_webhook_sent(payment, -1)
|
||||||
|
|
|
||||||
|
|
@ -213,6 +213,24 @@
|
||||||
label="Webhook URL (optional)"
|
label="Webhook URL (optional)"
|
||||||
hint="A URL to be called whenever this link receives a payment."
|
hint="A URL to be called whenever this link receives a payment."
|
||||||
></q-input>
|
></q-input>
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-if="formDialog.data.webhook_url"
|
||||||
|
v-model="formDialog.data.webhook_headers"
|
||||||
|
type="text"
|
||||||
|
label="Webhook headers (optional)"
|
||||||
|
hint="Custom data as JSON string, send headers along with the webhook."
|
||||||
|
></q-input>
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-if="formDialog.data.webhook_url"
|
||||||
|
v-model="formDialog.data.webhook_body"
|
||||||
|
type="text"
|
||||||
|
label="Webhook custom data (optional)"
|
||||||
|
hint="Custom data as JSON string, will get posted along with webhook 'body' field."
|
||||||
|
></q-input>
|
||||||
<q-input
|
<q-input
|
||||||
filled
|
filled
|
||||||
dense
|
dense
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import json
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
|
|
||||||
from fastapi import Request
|
from fastapi import Request
|
||||||
|
|
@ -90,6 +91,24 @@ async def api_link_create_or_update(
|
||||||
detail="Must use full satoshis.", status_code=HTTPStatus.BAD_REQUEST
|
detail="Must use full satoshis.", status_code=HTTPStatus.BAD_REQUEST
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if data.webhook_headers:
|
||||||
|
try:
|
||||||
|
json.loads(data.webhook_headers)
|
||||||
|
except ValueError:
|
||||||
|
raise HTTPException(
|
||||||
|
detail="Invalid JSON in webhook_headers.",
|
||||||
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
if data.webhook_body:
|
||||||
|
try:
|
||||||
|
json.loads(data.webhook_body)
|
||||||
|
except ValueError:
|
||||||
|
raise HTTPException(
|
||||||
|
detail="Invalid JSON in webhook_body.",
|
||||||
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
# database only allows int4 entries for min and max. For fiat currencies,
|
# database only allows int4 entries for min and max. For fiat currencies,
|
||||||
# we multiply by data.fiat_base_multiplier (usually 100) to save the value in cents.
|
# we multiply by data.fiat_base_multiplier (usually 100) to save the value in cents.
|
||||||
if data.currency and data.fiat_base_multiplier:
|
if data.currency and data.fiat_base_multiplier:
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,7 @@ async def api_lnurlpayout_delete(
|
||||||
)
|
)
|
||||||
|
|
||||||
await delete_lnurlpayout(lnurlpayout_id)
|
await delete_lnurlpayout(lnurlpayout_id)
|
||||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
return "", HTTPStatus.NO_CONTENT
|
||||||
|
|
||||||
|
|
||||||
@lnurlpayout_ext.get("/api/v1/lnurlpayouts/{lnurlpayout_id}", status_code=HTTPStatus.OK)
|
@lnurlpayout_ext.get("/api/v1/lnurlpayouts/{lnurlpayout_id}", status_code=HTTPStatus.OK)
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
# type: ignore
|
||||||
from os import getenv
|
from os import getenv
|
||||||
|
|
||||||
from fastapi import Request
|
from fastapi import Request
|
||||||
|
|
@ -34,7 +35,9 @@ ngrok_tunnel = ngrok.connect(port)
|
||||||
|
|
||||||
|
|
||||||
@ngrok_ext.get("/")
|
@ngrok_ext.get("/")
|
||||||
async def index(request: Request, user: User = Depends(check_user_exists)):
|
async def index(
|
||||||
|
request: Request, user: User = Depends(check_user_exists) # type: ignore
|
||||||
|
):
|
||||||
return ngrok_renderer().TemplateResponse(
|
return ngrok_renderer().TemplateResponse(
|
||||||
"ngrok/index.html", {"request": request, "ngrok": string5, "user": user.dict()}
|
"ngrok/index.html", {"request": request, "ngrok": string5, "user": user.dict()}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,9 @@
|
||||||
|
|
||||||
LNBits Offline Shop allows for merchants to receive Bitcoin payments while offline and without any electronic device.
|
LNBits Offline Shop allows for merchants to receive Bitcoin payments while offline and without any electronic device.
|
||||||
|
|
||||||
Merchant will create items and associate a QR code ([a LNURLp](https://github.com/lnbits/lnbits/blob/master/lnbits/extensions/lnurlp/README.md)) with a price. He can then print the QR codes and display them on their shop. When a costumer chooses an item, scans the QR code, gets the description and price. After payment, the costumer gets a confirmation code that the merchant can validate to be sure the payment was successful.
|
Merchant will create items and associate a QR code ([a LNURLp](https://github.com/lnbits/lnbits/blob/master/lnbits/extensions/lnurlp/README.md)) with a price. He can then print the QR codes and display them on their shop. When a customer chooses an item, scans the QR code, gets the description and price. After payment, the customer gets a confirmation code that the merchant can validate to be sure the payment was successful.
|
||||||
|
|
||||||
Costumers must use an LNURL pay capable wallet.
|
Customers must use an LNURL pay capable wallet.
|
||||||
|
|
||||||
[**Wallets supporting LNURL**](https://github.com/fiatjaf/awesome-lnurl#wallets)
|
[**Wallets supporting LNURL**](https://github.com/fiatjaf/awesome-lnurl#wallets)
|
||||||
|
|
||||||
|
|
@ -18,18 +18,18 @@ Costumers must use an LNURL pay capable wallet.
|
||||||

|

|
||||||
2. Begin by creating an item, click "ADD NEW ITEM"
|
2. Begin by creating an item, click "ADD NEW ITEM"
|
||||||
- set the item name and a small description
|
- set the item name and a small description
|
||||||
- you can set an optional, preferably square image, that will show up on the costumer wallet - _depending on wallet_
|
- you can set an optional, preferably square image, that will show up on the customer wallet - _depending on wallet_
|
||||||
- set the item price, if you choose a fiat currency the bitcoin conversion will happen at the time costumer scans to pay\
|
- set the item price, if you choose a fiat currency the bitcoin conversion will happen at the time customer scans to pay\
|
||||||

|

|
||||||
3. After creating some products, click on "PRINT QR CODES"\
|
3. After creating some products, click on "PRINT QR CODES"\
|
||||||

|

|
||||||
4. You'll see a QR code for each product in your LNBits Offline Shop with a title and price ready for printing\
|
4. You'll see a QR code for each product in your LNBits Offline Shop with a title and price ready for printing\
|
||||||

|

|
||||||
5. Place the printed QR codes on your shop, or at the fair stall, or have them as a menu style laminated sheet
|
5. Place the printed QR codes on your shop, or at the fair stall, or have them as a menu style laminated sheet
|
||||||
6. Choose what type of confirmation do you want costumers to report to merchant after a successful payment\
|
6. Choose what type of confirmation do you want customers to report to merchant after a successful payment\
|
||||||

|

|
||||||
|
|
||||||
- Wordlist is the default option: after a successful payment the costumer will receive a word from this list, **sequentially**. Starting in _albatross_ as costumers pay for the items they will get the next word in the list until _zebra_, then it starts at the top again. The list can be changed, for example if you think A-Z is a big list to track, you can use _apple_, _banana_, _coconut_\
|
- Wordlist is the default option: after a successful payment the customer will receive a word from this list, **sequentially**. Starting in _albatross_ as customers pay for the items they will get the next word in the list until _zebra_, then it starts at the top again. The list can be changed, for example if you think A-Z is a big list to track, you can use _apple_, _banana_, _coconut_\
|
||||||

|

|
||||||
- TOTP (time-based one time password) can be used instead. If you use Google Authenticator just scan the presented QR with the app and after a successful payment the user will get the password that you can check with GA\
|
- TOTP (time-based one time password) can be used instead. If you use Google Authenticator just scan the presented QR with the app and after a successful payment the user will get the password that you can check with GA\
|
||||||

|

|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,7 @@ async def api_add_or_update_item(
|
||||||
async def api_delete_item(item_id, wallet: WalletTypeInfo = Depends(get_key_type)):
|
async def api_delete_item(item_id, wallet: WalletTypeInfo = Depends(get_key_type)):
|
||||||
shop = await get_or_create_shop_by_wallet(wallet.wallet.id)
|
shop = await get_or_create_shop_by_wallet(wallet.wallet.id)
|
||||||
await delete_item_from_shop(shop.id, item_id)
|
await delete_item_from_shop(shop.id, item_id)
|
||||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
return "", HTTPStatus.NO_CONTENT
|
||||||
|
|
||||||
|
|
||||||
class CreateMethodData(BaseModel):
|
class CreateMethodData(BaseModel):
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ async def api_paywall_delete(
|
||||||
)
|
)
|
||||||
|
|
||||||
await delete_paywall(paywall_id)
|
await delete_paywall(paywall_id)
|
||||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
return "", HTTPStatus.NO_CONTENT
|
||||||
|
|
||||||
|
|
||||||
@paywall_ext.post("/api/v1/paywalls/invoice/{paywall_id}")
|
@paywall_ext.post("/api/v1/paywalls/invoice/{paywall_id}")
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ from .models import (
|
||||||
CreateSatsDiceLink,
|
CreateSatsDiceLink,
|
||||||
CreateSatsDicePayment,
|
CreateSatsDicePayment,
|
||||||
CreateSatsDiceWithdraw,
|
CreateSatsDiceWithdraw,
|
||||||
HashCheck,
|
|
||||||
satsdiceLink,
|
satsdiceLink,
|
||||||
satsdicePayment,
|
satsdicePayment,
|
||||||
satsdiceWithdraw,
|
satsdiceWithdraw,
|
||||||
|
|
@ -76,7 +75,7 @@ async def get_satsdice_pays(wallet_ids: Union[str, List[str]]) -> List[satsdiceL
|
||||||
return [satsdiceLink(**row) for row in rows]
|
return [satsdiceLink(**row) for row in rows]
|
||||||
|
|
||||||
|
|
||||||
async def update_satsdice_pay(link_id: int, **kwargs) -> Optional[satsdiceLink]:
|
async def update_satsdice_pay(link_id: str, **kwargs) -> satsdiceLink:
|
||||||
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
|
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
|
||||||
await db.execute(
|
await db.execute(
|
||||||
f"UPDATE satsdice.satsdice_pay SET {q} WHERE id = ?",
|
f"UPDATE satsdice.satsdice_pay SET {q} WHERE id = ?",
|
||||||
|
|
@ -85,10 +84,10 @@ async def update_satsdice_pay(link_id: int, **kwargs) -> Optional[satsdiceLink]:
|
||||||
row = await db.fetchone(
|
row = await db.fetchone(
|
||||||
"SELECT * FROM satsdice.satsdice_pay WHERE id = ?", (link_id,)
|
"SELECT * FROM satsdice.satsdice_pay WHERE id = ?", (link_id,)
|
||||||
)
|
)
|
||||||
return satsdiceLink(**row) if row else None
|
return satsdiceLink(**row)
|
||||||
|
|
||||||
|
|
||||||
async def increment_satsdice_pay(link_id: int, **kwargs) -> Optional[satsdiceLink]:
|
async def increment_satsdice_pay(link_id: str, **kwargs) -> Optional[satsdiceLink]:
|
||||||
q = ", ".join([f"{field[0]} = {field[0]} + ?" for field in kwargs.items()])
|
q = ", ".join([f"{field[0]} = {field[0]} + ?" for field in kwargs.items()])
|
||||||
await db.execute(
|
await db.execute(
|
||||||
f"UPDATE satsdice.satsdice_pay SET {q} WHERE id = ?",
|
f"UPDATE satsdice.satsdice_pay SET {q} WHERE id = ?",
|
||||||
|
|
@ -100,7 +99,7 @@ async def increment_satsdice_pay(link_id: int, **kwargs) -> Optional[satsdiceLin
|
||||||
return satsdiceLink(**row) if row else None
|
return satsdiceLink(**row) if row else None
|
||||||
|
|
||||||
|
|
||||||
async def delete_satsdice_pay(link_id: int) -> None:
|
async def delete_satsdice_pay(link_id: str) -> None:
|
||||||
await db.execute("DELETE FROM satsdice.satsdice_pay WHERE id = ?", (link_id,))
|
await db.execute("DELETE FROM satsdice.satsdice_pay WHERE id = ?", (link_id,))
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -119,9 +118,15 @@ async def create_satsdice_payment(data: CreateSatsDicePayment) -> satsdicePaymen
|
||||||
)
|
)
|
||||||
VALUES (?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?)
|
||||||
""",
|
""",
|
||||||
(data["payment_hash"], data["satsdice_pay"], data["value"], False, False),
|
(
|
||||||
|
data.payment_hash,
|
||||||
|
data.satsdice_pay,
|
||||||
|
data.value,
|
||||||
|
False,
|
||||||
|
False,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
payment = await get_satsdice_payment(data["payment_hash"])
|
payment = await get_satsdice_payment(data.payment_hash)
|
||||||
assert payment, "Newly created withdraw couldn't be retrieved"
|
assert payment, "Newly created withdraw couldn't be retrieved"
|
||||||
return payment
|
return payment
|
||||||
|
|
||||||
|
|
@ -134,9 +139,7 @@ async def get_satsdice_payment(payment_hash: str) -> Optional[satsdicePayment]:
|
||||||
return satsdicePayment(**row) if row else None
|
return satsdicePayment(**row) if row else None
|
||||||
|
|
||||||
|
|
||||||
async def update_satsdice_payment(
|
async def update_satsdice_payment(payment_hash: str, **kwargs) -> satsdicePayment:
|
||||||
payment_hash: int, **kwargs
|
|
||||||
) -> Optional[satsdicePayment]:
|
|
||||||
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
|
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
|
||||||
|
|
||||||
await db.execute(
|
await db.execute(
|
||||||
|
|
@ -147,7 +150,7 @@ async def update_satsdice_payment(
|
||||||
"SELECT * FROM satsdice.satsdice_payment WHERE payment_hash = ?",
|
"SELECT * FROM satsdice.satsdice_payment WHERE payment_hash = ?",
|
||||||
(payment_hash,),
|
(payment_hash,),
|
||||||
)
|
)
|
||||||
return satsdicePayment(**row) if row else None
|
return satsdicePayment(**row)
|
||||||
|
|
||||||
|
|
||||||
##################SATSDICE WITHDRAW LINKS
|
##################SATSDICE WITHDRAW LINKS
|
||||||
|
|
@ -168,16 +171,16 @@ async def create_satsdice_withdraw(data: CreateSatsDiceWithdraw) -> satsdiceWith
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
data["payment_hash"],
|
data.payment_hash,
|
||||||
data["satsdice_pay"],
|
data.satsdice_pay,
|
||||||
data["value"],
|
data.value,
|
||||||
urlsafe_short_hash(),
|
urlsafe_short_hash(),
|
||||||
urlsafe_short_hash(),
|
urlsafe_short_hash(),
|
||||||
int(datetime.now().timestamp()),
|
int(datetime.now().timestamp()),
|
||||||
data["used"],
|
data.used,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
withdraw = await get_satsdice_withdraw(data["payment_hash"], 0)
|
withdraw = await get_satsdice_withdraw(data.payment_hash, 0)
|
||||||
assert withdraw, "Newly created withdraw couldn't be retrieved"
|
assert withdraw, "Newly created withdraw couldn't be retrieved"
|
||||||
return withdraw
|
return withdraw
|
||||||
|
|
||||||
|
|
@ -247,7 +250,7 @@ async def delete_satsdice_withdraw(withdraw_id: str) -> None:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def create_withdraw_hash_check(the_hash: str, lnurl_id: str) -> HashCheck:
|
async def create_withdraw_hash_check(the_hash: str, lnurl_id: str):
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO satsdice.hash_checkw (
|
INSERT INTO satsdice.hash_checkw (
|
||||||
|
|
@ -262,18 +265,14 @@ async def create_withdraw_hash_check(the_hash: str, lnurl_id: str) -> HashCheck:
|
||||||
return hashCheck
|
return hashCheck
|
||||||
|
|
||||||
|
|
||||||
async def get_withdraw_hash_checkw(the_hash: str, lnurl_id: str) -> Optional[HashCheck]:
|
async def get_withdraw_hash_checkw(the_hash: str, lnurl_id: str):
|
||||||
rowid = await db.fetchone(
|
rowid = await db.fetchone(
|
||||||
"SELECT * FROM satsdice.hash_checkw WHERE id = ?", (the_hash,)
|
"SELECT * FROM satsdice.hash_checkw WHERE id = ?", (the_hash,)
|
||||||
)
|
)
|
||||||
rowlnurl = await db.fetchone(
|
rowlnurl = await db.fetchone(
|
||||||
"SELECT * FROM satsdice.hash_checkw WHERE lnurl_id = ?", (lnurl_id,)
|
"SELECT * FROM satsdice.hash_checkw WHERE lnurl_id = ?", (lnurl_id,)
|
||||||
)
|
)
|
||||||
if not rowlnurl:
|
if not rowlnurl or not rowid:
|
||||||
await create_withdraw_hash_check(the_hash, lnurl_id)
|
|
||||||
return {"lnurl": True, "hash": False}
|
|
||||||
else:
|
|
||||||
if not rowid:
|
|
||||||
await create_withdraw_hash_check(the_hash, lnurl_id)
|
await create_withdraw_hash_check(the_hash, lnurl_id)
|
||||||
return {"lnurl": True, "hash": False}
|
return {"lnurl": True, "hash": False}
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import hashlib
|
|
||||||
import json
|
import json
|
||||||
import math
|
import math
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
|
|
@ -83,15 +82,18 @@ async def api_lnurlp_callback(
|
||||||
|
|
||||||
success_action = link.success_action(payment_hash=payment_hash, req=req)
|
success_action = link.success_action(payment_hash=payment_hash, req=req)
|
||||||
|
|
||||||
data: CreateSatsDicePayment = {
|
data = CreateSatsDicePayment(
|
||||||
"satsdice_pay": link.id,
|
satsdice_pay=link.id,
|
||||||
"value": amount_received / 1000,
|
value=amount_received / 1000,
|
||||||
"payment_hash": payment_hash,
|
payment_hash=payment_hash,
|
||||||
}
|
)
|
||||||
|
|
||||||
await create_satsdice_payment(data)
|
await create_satsdice_payment(data)
|
||||||
payResponse = {"pr": payment_request, "successAction": success_action, "routes": []}
|
payResponse: dict = {
|
||||||
|
"pr": payment_request,
|
||||||
|
"successAction": success_action,
|
||||||
|
"routes": [],
|
||||||
|
}
|
||||||
return json.dumps(payResponse)
|
return json.dumps(payResponse)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -133,9 +135,7 @@ async def api_lnurlw_response(req: Request, unique_hash: str = Query(None)):
|
||||||
name="satsdice.api_lnurlw_callback",
|
name="satsdice.api_lnurlw_callback",
|
||||||
)
|
)
|
||||||
async def api_lnurlw_callback(
|
async def api_lnurlw_callback(
|
||||||
req: Request,
|
|
||||||
unique_hash: str = Query(None),
|
unique_hash: str = Query(None),
|
||||||
k1: str = Query(None),
|
|
||||||
pr: str = Query(None),
|
pr: str = Query(None),
|
||||||
):
|
):
|
||||||
|
|
||||||
|
|
@ -146,6 +146,7 @@ async def api_lnurlw_callback(
|
||||||
return {"status": "ERROR", "reason": "spent"}
|
return {"status": "ERROR", "reason": "spent"}
|
||||||
paylink = await get_satsdice_pay(link.satsdice_pay)
|
paylink = await get_satsdice_pay(link.satsdice_pay)
|
||||||
|
|
||||||
|
if paylink:
|
||||||
await update_satsdice_withdraw(link.id, used=1)
|
await update_satsdice_withdraw(link.id, used=1)
|
||||||
await pay_invoice(
|
await pay_invoice(
|
||||||
wallet_id=paylink.wallet,
|
wallet_id=paylink.wallet,
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ from typing import Dict, Optional
|
||||||
|
|
||||||
from fastapi import Request
|
from fastapi import Request
|
||||||
from fastapi.param_functions import Query
|
from fastapi.param_functions import Query
|
||||||
from lnurl import Lnurl, LnurlWithdrawResponse
|
from lnurl import Lnurl
|
||||||
from lnurl import encode as lnurl_encode # type: ignore
|
from lnurl import encode as lnurl_encode # type: ignore
|
||||||
from lnurl.types import LnurlPayMetadata # type: ignore
|
from lnurl.types import LnurlPayMetadata # type: ignore
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
@ -80,8 +80,7 @@ class satsdiceWithdraw(BaseModel):
|
||||||
def is_spent(self) -> bool:
|
def is_spent(self) -> bool:
|
||||||
return self.used >= 1
|
return self.used >= 1
|
||||||
|
|
||||||
@property
|
def lnurl_response(self, req: Request):
|
||||||
def lnurl_response(self, req: Request) -> LnurlWithdrawResponse:
|
|
||||||
url = req.url_for("satsdice.api_lnurlw_callback", unique_hash=self.unique_hash)
|
url = req.url_for("satsdice.api_lnurlw_callback", unique_hash=self.unique_hash)
|
||||||
withdrawResponse = {
|
withdrawResponse = {
|
||||||
"tag": "withdrawRequest",
|
"tag": "withdrawRequest",
|
||||||
|
|
@ -99,7 +98,7 @@ class HashCheck(BaseModel):
|
||||||
lnurl_id: str
|
lnurl_id: str
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_row(cls, row: Row) -> "Hash":
|
def from_row(cls, row: Row):
|
||||||
return cls(**dict(row))
|
return cls(**dict(row))
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import random
|
import random
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
import pyqrcode
|
||||||
from fastapi import Request
|
from fastapi import Request
|
||||||
from fastapi.param_functions import Query
|
from fastapi.param_functions import Query
|
||||||
from fastapi.params import Depends
|
from fastapi.params import Depends
|
||||||
|
|
@ -20,13 +22,15 @@ from .crud import (
|
||||||
get_satsdice_withdraw,
|
get_satsdice_withdraw,
|
||||||
update_satsdice_payment,
|
update_satsdice_payment,
|
||||||
)
|
)
|
||||||
from .models import CreateSatsDiceWithdraw, satsdiceLink
|
from .models import CreateSatsDiceWithdraw
|
||||||
|
|
||||||
templates = Jinja2Templates(directory="templates")
|
templates = Jinja2Templates(directory="templates")
|
||||||
|
|
||||||
|
|
||||||
@satsdice_ext.get("/", response_class=HTMLResponse)
|
@satsdice_ext.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) # type: ignore
|
||||||
|
):
|
||||||
return satsdice_renderer().TemplateResponse(
|
return satsdice_renderer().TemplateResponse(
|
||||||
"satsdice/index.html", {"request": request, "user": user.dict()}
|
"satsdice/index.html", {"request": request, "user": user.dict()}
|
||||||
)
|
)
|
||||||
|
|
@ -67,7 +71,7 @@ async def displaywin(
|
||||||
)
|
)
|
||||||
withdrawLink = await get_satsdice_withdraw(payment_hash)
|
withdrawLink = await get_satsdice_withdraw(payment_hash)
|
||||||
payment = await get_satsdice_payment(payment_hash)
|
payment = await get_satsdice_payment(payment_hash)
|
||||||
if payment.lost:
|
if not payment or payment.lost:
|
||||||
return satsdice_renderer().TemplateResponse(
|
return satsdice_renderer().TemplateResponse(
|
||||||
"satsdice/error.html",
|
"satsdice/error.html",
|
||||||
{"request": request, "link": satsdicelink.id, "paid": False, "lost": True},
|
{"request": request, "link": satsdicelink.id, "paid": False, "lost": True},
|
||||||
|
|
@ -96,13 +100,18 @@ async def displaywin(
|
||||||
)
|
)
|
||||||
await update_satsdice_payment(payment_hash, paid=1)
|
await update_satsdice_payment(payment_hash, paid=1)
|
||||||
paylink = await get_satsdice_payment(payment_hash)
|
paylink = await get_satsdice_payment(payment_hash)
|
||||||
|
if not paylink:
|
||||||
|
return satsdice_renderer().TemplateResponse(
|
||||||
|
"satsdice/error.html",
|
||||||
|
{"request": request, "link": satsdicelink.id, "paid": False, "lost": True},
|
||||||
|
)
|
||||||
|
|
||||||
data: CreateSatsDiceWithdraw = {
|
data = CreateSatsDiceWithdraw(
|
||||||
"satsdice_pay": satsdicelink.id,
|
satsdice_pay=satsdicelink.id,
|
||||||
"value": paylink.value * satsdicelink.multiplier,
|
value=paylink.value * satsdicelink.multiplier,
|
||||||
"payment_hash": payment_hash,
|
payment_hash=payment_hash,
|
||||||
"used": 0,
|
used=0,
|
||||||
}
|
)
|
||||||
|
|
||||||
withdrawLink = await create_satsdice_withdraw(data)
|
withdrawLink = await create_satsdice_withdraw(data)
|
||||||
return satsdice_renderer().TemplateResponse(
|
return satsdice_renderer().TemplateResponse(
|
||||||
|
|
@ -121,9 +130,12 @@ async def displaywin(
|
||||||
|
|
||||||
@satsdice_ext.get("/img/{link_id}", response_class=HTMLResponse)
|
@satsdice_ext.get("/img/{link_id}", response_class=HTMLResponse)
|
||||||
async def img(link_id):
|
async def img(link_id):
|
||||||
link = await get_satsdice_pay(link_id) or abort(
|
link = await get_satsdice_pay(link_id)
|
||||||
HTTPStatus.NOT_FOUND, "satsdice link does not exist."
|
if not link:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.NOT_FOUND, detail="satsdice link does not exist."
|
||||||
)
|
)
|
||||||
|
|
||||||
qr = pyqrcode.create(link.lnurl)
|
qr = pyqrcode.create(link.lnurl)
|
||||||
stream = BytesIO()
|
stream = BytesIO()
|
||||||
qr.svg(stream, scale=3)
|
qr.svg(stream, scale=3)
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,10 @@ from .crud import (
|
||||||
delete_satsdice_pay,
|
delete_satsdice_pay,
|
||||||
get_satsdice_pay,
|
get_satsdice_pay,
|
||||||
get_satsdice_pays,
|
get_satsdice_pays,
|
||||||
|
get_withdraw_hash_checkw,
|
||||||
update_satsdice_pay,
|
update_satsdice_pay,
|
||||||
)
|
)
|
||||||
from .models import CreateSatsDiceLink, CreateSatsDiceWithdraws, satsdiceLink
|
from .models import CreateSatsDiceLink
|
||||||
|
|
||||||
################LNURL pay
|
################LNURL pay
|
||||||
|
|
||||||
|
|
@ -25,13 +26,15 @@ from .models import CreateSatsDiceLink, CreateSatsDiceWithdraws, satsdiceLink
|
||||||
@satsdice_ext.get("/api/v1/links")
|
@satsdice_ext.get("/api/v1/links")
|
||||||
async def api_links(
|
async def api_links(
|
||||||
request: Request,
|
request: Request,
|
||||||
wallet: WalletTypeInfo = Depends(get_key_type),
|
wallet: WalletTypeInfo = Depends(get_key_type), # type: ignore
|
||||||
all_wallets: bool = Query(False),
|
all_wallets: bool = Query(False),
|
||||||
):
|
):
|
||||||
wallet_ids = [wallet.wallet.id]
|
wallet_ids = [wallet.wallet.id]
|
||||||
|
|
||||||
if all_wallets:
|
if all_wallets:
|
||||||
wallet_ids = (await get_user(wallet.wallet.user)).wallet_ids
|
user = await get_user(wallet.wallet.user)
|
||||||
|
if user:
|
||||||
|
wallet_ids = user.wallet_ids
|
||||||
|
|
||||||
try:
|
try:
|
||||||
links = await get_satsdice_pays(wallet_ids)
|
links = await get_satsdice_pays(wallet_ids)
|
||||||
|
|
@ -46,7 +49,7 @@ async def api_links(
|
||||||
|
|
||||||
@satsdice_ext.get("/api/v1/links/{link_id}")
|
@satsdice_ext.get("/api/v1/links/{link_id}")
|
||||||
async def api_link_retrieve(
|
async def api_link_retrieve(
|
||||||
link_id: str = Query(None), wallet: WalletTypeInfo = Depends(get_key_type)
|
link_id: str = Query(None), wallet: WalletTypeInfo = Depends(get_key_type) # type: ignore
|
||||||
):
|
):
|
||||||
link = await get_satsdice_pay(link_id)
|
link = await get_satsdice_pay(link_id)
|
||||||
|
|
||||||
|
|
@ -67,7 +70,7 @@ async def api_link_retrieve(
|
||||||
@satsdice_ext.put("/api/v1/links/{link_id}", status_code=HTTPStatus.OK)
|
@satsdice_ext.put("/api/v1/links/{link_id}", status_code=HTTPStatus.OK)
|
||||||
async def api_link_create_or_update(
|
async def api_link_create_or_update(
|
||||||
data: CreateSatsDiceLink,
|
data: CreateSatsDiceLink,
|
||||||
wallet: WalletTypeInfo = Depends(get_key_type),
|
wallet: WalletTypeInfo = Depends(get_key_type), # type: ignore
|
||||||
link_id: str = Query(None),
|
link_id: str = Query(None),
|
||||||
):
|
):
|
||||||
if data.min_bet > data.max_bet:
|
if data.min_bet > data.max_bet:
|
||||||
|
|
@ -95,10 +98,10 @@ async def api_link_create_or_update(
|
||||||
|
|
||||||
@satsdice_ext.delete("/api/v1/links/{link_id}")
|
@satsdice_ext.delete("/api/v1/links/{link_id}")
|
||||||
async def api_link_delete(
|
async def api_link_delete(
|
||||||
wallet: WalletTypeInfo = Depends(get_key_type), link_id: str = Query(None)
|
wallet: WalletTypeInfo = Depends(get_key_type), # type: ignore
|
||||||
|
link_id: str = Query(None),
|
||||||
):
|
):
|
||||||
link = await get_satsdice_pay(link_id)
|
link = await get_satsdice_pay(link_id)
|
||||||
|
|
||||||
if not link:
|
if not link:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.NOT_FOUND, detail="Pay link does not exist."
|
status_code=HTTPStatus.NOT_FOUND, detail="Pay link does not exist."
|
||||||
|
|
@ -117,11 +120,12 @@ async def api_link_delete(
|
||||||
##########LNURL withdraw
|
##########LNURL withdraw
|
||||||
|
|
||||||
|
|
||||||
@satsdice_ext.get("/api/v1/withdraws/{the_hash}/{lnurl_id}")
|
@satsdice_ext.get(
|
||||||
|
"/api/v1/withdraws/{the_hash}/{lnurl_id}", dependencies=[Depends(get_key_type)]
|
||||||
|
)
|
||||||
async def api_withdraw_hash_retrieve(
|
async def api_withdraw_hash_retrieve(
|
||||||
wallet: WalletTypeInfo = Depends(get_key_type),
|
|
||||||
lnurl_id: str = Query(None),
|
lnurl_id: str = Query(None),
|
||||||
the_hash: str = Query(None),
|
the_hash: str = Query(None),
|
||||||
):
|
):
|
||||||
hashCheck = await get_withdraw_hash_check(the_hash, lnurl_id)
|
hashCheck = await get_withdraw_hash_checkw(the_hash, lnurl_id)
|
||||||
return hashCheck
|
return hashCheck
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ Easilly create invoices that support Lightning Network and on-chain BTC payment.
|
||||||

|

|
||||||
3. The charge will appear on the _Charges_ section\
|
3. The charge will appear on the _Charges_ section\
|
||||||

|

|
||||||
4. Your costumer/payee will get the payment page
|
4. Your customer/payee will get the payment page
|
||||||
- they can choose to pay on LN\
|
- they can choose to pay on LN\
|
||||||

|

|
||||||
- or pay on chain\
|
- or pay on chain\
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,7 @@ async def check_address_balance(charge_id: str) -> List[Charges]:
|
||||||
charge = await get_charge(charge_id)
|
charge = await get_charge(charge_id)
|
||||||
if not charge.paid:
|
if not charge.paid:
|
||||||
if charge.onchainaddress:
|
if charge.onchainaddress:
|
||||||
config = await get_config(charge.user)
|
config = await get_charge_config(charge_id)
|
||||||
try:
|
try:
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
r = await client.get(
|
r = await client.get(
|
||||||
|
|
@ -122,3 +122,10 @@ async def check_address_balance(charge_id: str) -> List[Charges]:
|
||||||
return await update_charge(charge_id=charge_id, balance=charge.amount)
|
return await update_charge(charge_id=charge_id, balance=charge.amount)
|
||||||
row = await db.fetchone("SELECT * FROM satspay.charges WHERE id = ?", (charge_id,))
|
row = await db.fetchone("SELECT * FROM satspay.charges WHERE id = ?", (charge_id,))
|
||||||
return Charges.from_row(row) if row else None
|
return Charges.from_row(row) if row else None
|
||||||
|
|
||||||
|
|
||||||
|
async def get_charge_config(charge_id: str):
|
||||||
|
row = await db.fetchone(
|
||||||
|
"""SELECT "user" FROM satspay.charges WHERE id = ?""", (charge_id,)
|
||||||
|
)
|
||||||
|
return await get_config(row.user)
|
||||||
|
|
|
||||||
17
lnbits/extensions/satspay/helpers.py
Normal file
17
lnbits/extensions/satspay/helpers.py
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
from .models import Charges
|
||||||
|
|
||||||
|
|
||||||
|
def compact_charge(charge: Charges):
|
||||||
|
return {
|
||||||
|
"id": charge.id,
|
||||||
|
"description": charge.description,
|
||||||
|
"onchainaddress": charge.onchainaddress,
|
||||||
|
"payment_request": charge.payment_request,
|
||||||
|
"payment_hash": charge.payment_hash,
|
||||||
|
"time": charge.time,
|
||||||
|
"amount": charge.amount,
|
||||||
|
"balance": charge.balance,
|
||||||
|
"paid": charge.paid,
|
||||||
|
"timestamp": charge.timestamp,
|
||||||
|
"completelink": charge.completelink, # should be secret?
|
||||||
|
}
|
||||||
|
|
@ -19,7 +19,6 @@ class CreateCharge(BaseModel):
|
||||||
|
|
||||||
class Charges(BaseModel):
|
class Charges(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
user: str
|
|
||||||
description: Optional[str]
|
description: Optional[str]
|
||||||
onchainwallet: Optional[str]
|
onchainwallet: Optional[str]
|
||||||
onchainaddress: Optional[str]
|
onchainaddress: Optional[str]
|
||||||
|
|
|
||||||
|
|
@ -328,7 +328,7 @@
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
checkBalances: async function () {
|
checkBalances: async function () {
|
||||||
if (!this.charge.hasStaleBalance) await this.refreshCharge()
|
if (this.charge.hasStaleBalance) return
|
||||||
try {
|
try {
|
||||||
const {data} = await LNbits.api.request(
|
const {data} = await LNbits.api.request(
|
||||||
'GET',
|
'GET',
|
||||||
|
|
@ -339,18 +339,9 @@
|
||||||
LNbits.utils.notifyApiError(error)
|
LNbits.utils.notifyApiError(error)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
refreshCharge: async function () {
|
|
||||||
try {
|
|
||||||
const {data} = await LNbits.api.request(
|
|
||||||
'GET',
|
|
||||||
`/satspay/api/v1/charge/${this.charge.id}`
|
|
||||||
)
|
|
||||||
this.charge = mapCharge(data, this.charge)
|
|
||||||
} catch (error) {
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
checkPendingOnchain: async function () {
|
checkPendingOnchain: async function () {
|
||||||
|
if (!this.charge.onchainaddress) return
|
||||||
|
|
||||||
const {
|
const {
|
||||||
bitcoin: {addresses: addressesAPI}
|
bitcoin: {addresses: addressesAPI}
|
||||||
} = mempoolJS({
|
} = mempoolJS({
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,9 @@ from starlette.responses import HTMLResponse
|
||||||
from lnbits.core.crud import get_wallet
|
from lnbits.core.crud import get_wallet
|
||||||
from lnbits.core.models import User
|
from lnbits.core.models import User
|
||||||
from lnbits.decorators import check_user_exists
|
from lnbits.decorators import check_user_exists
|
||||||
from lnbits.extensions.watchonly.crud import get_config
|
|
||||||
|
|
||||||
from . import satspay_ext, satspay_renderer
|
from . import satspay_ext, satspay_renderer
|
||||||
from .crud import get_charge
|
from .crud import get_charge, get_charge_config
|
||||||
|
|
||||||
templates = Jinja2Templates(directory="templates")
|
templates = Jinja2Templates(directory="templates")
|
||||||
|
|
||||||
|
|
@ -32,7 +31,7 @@ async def display(request: Request, charge_id: str):
|
||||||
status_code=HTTPStatus.NOT_FOUND, detail="Charge link does not exist."
|
status_code=HTTPStatus.NOT_FOUND, detail="Charge link does not exist."
|
||||||
)
|
)
|
||||||
wallet = await get_wallet(charge.lnbitswallet)
|
wallet = await get_wallet(charge.lnbitswallet)
|
||||||
onchainwallet_config = await get_config(charge.user)
|
onchainwallet_config = await get_charge_config(charge_id)
|
||||||
inkey = wallet.inkey if wallet else None
|
inkey = wallet.inkey if wallet else None
|
||||||
mempool_endpoint = (
|
mempool_endpoint = (
|
||||||
onchainwallet_config.mempool_endpoint if onchainwallet_config else None
|
onchainwallet_config.mempool_endpoint if onchainwallet_config else None
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ from .crud import (
|
||||||
get_charges,
|
get_charges,
|
||||||
update_charge,
|
update_charge,
|
||||||
)
|
)
|
||||||
|
from .helpers import compact_charge
|
||||||
from .models import CreateCharge
|
from .models import CreateCharge
|
||||||
|
|
||||||
#############################CHARGES##########################
|
#############################CHARGES##########################
|
||||||
|
|
@ -93,7 +94,7 @@ async def api_charge_delete(charge_id, wallet: WalletTypeInfo = Depends(get_key_
|
||||||
)
|
)
|
||||||
|
|
||||||
await delete_charge(charge_id)
|
await delete_charge(charge_id)
|
||||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
return "", HTTPStatus.NO_CONTENT
|
||||||
|
|
||||||
|
|
||||||
#############################BALANCE##########################
|
#############################BALANCE##########################
|
||||||
|
|
@ -123,25 +124,13 @@ async def api_charge_balance(charge_id):
|
||||||
try:
|
try:
|
||||||
r = await client.post(
|
r = await client.post(
|
||||||
charge.webhook,
|
charge.webhook,
|
||||||
json={
|
json=compact_charge(charge),
|
||||||
"id": charge.id,
|
|
||||||
"description": charge.description,
|
|
||||||
"onchainaddress": charge.onchainaddress,
|
|
||||||
"payment_request": charge.payment_request,
|
|
||||||
"payment_hash": charge.payment_hash,
|
|
||||||
"time": charge.time,
|
|
||||||
"amount": charge.amount,
|
|
||||||
"balance": charge.balance,
|
|
||||||
"paid": charge.paid,
|
|
||||||
"timestamp": charge.timestamp,
|
|
||||||
"completelink": charge.completelink,
|
|
||||||
},
|
|
||||||
timeout=40,
|
timeout=40,
|
||||||
)
|
)
|
||||||
except AssertionError:
|
except AssertionError:
|
||||||
charge.webhook = None
|
charge.webhook = None
|
||||||
return {
|
return {
|
||||||
**charge.dict(),
|
**compact_charge(charge),
|
||||||
**{"time_elapsed": charge.time_elapsed},
|
**{"time_elapsed": charge.time_elapsed},
|
||||||
**{"time_left": charge.time_left},
|
**{"time_left": charge.time_left},
|
||||||
**{"paid": charge.paid},
|
**{"paid": charge.paid},
|
||||||
|
|
|
||||||
|
|
@ -109,4 +109,4 @@ async def api_link_delete(link_id, wallet: WalletTypeInfo = Depends(require_admi
|
||||||
)
|
)
|
||||||
|
|
||||||
await delete_scrub_link(link_id)
|
await delete_scrub_link(link_id)
|
||||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
return "", HTTPStatus.NO_CONTENT
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ class Target(BaseModel):
|
||||||
class TargetPutList(BaseModel):
|
class TargetPutList(BaseModel):
|
||||||
wallet: str = Query(...)
|
wallet: str = Query(...)
|
||||||
alias: str = Query("")
|
alias: str = Query("")
|
||||||
percent: float = Query(..., ge=0.01)
|
percent: float = Query(..., ge=0.01, lt=100)
|
||||||
|
|
||||||
|
|
||||||
class TargetPut(BaseModel):
|
class TargetPut(BaseModel):
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,11 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
import json
|
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from lnbits.core import db as core_db
|
|
||||||
from lnbits.core.crud import create_payment
|
|
||||||
from lnbits.core.models import Payment
|
from lnbits.core.models import Payment
|
||||||
from lnbits.helpers import get_current_extension_name, urlsafe_short_hash
|
from lnbits.core.services import create_invoice, pay_invoice
|
||||||
from lnbits.tasks import internal_invoice_queue, register_invoice_listener
|
from lnbits.helpers import get_current_extension_name
|
||||||
|
from lnbits.tasks import register_invoice_listener
|
||||||
|
|
||||||
from .crud import get_targets
|
from .crud import get_targets
|
||||||
|
|
||||||
|
|
@ -22,60 +20,36 @@ async def wait_for_paid_invoices():
|
||||||
|
|
||||||
|
|
||||||
async def on_invoice_paid(payment: Payment) -> None:
|
async def on_invoice_paid(payment: Payment) -> None:
|
||||||
if payment.extra.get("tag") == "splitpayments" or payment.extra.get("splitted"):
|
if payment.extra.get("tag") == "splitpayments":
|
||||||
# already splitted, ignore
|
# already a splitted payment, ignore
|
||||||
return
|
return
|
||||||
|
|
||||||
# now we make some special internal transfers (from no one to the receiver)
|
|
||||||
targets = await get_targets(payment.wallet_id)
|
targets = await get_targets(payment.wallet_id)
|
||||||
|
|
||||||
if not targets:
|
if not targets:
|
||||||
return
|
return
|
||||||
|
|
||||||
transfers = [
|
total_percent = sum([target.percent for target in targets])
|
||||||
(target.wallet, int(target.percent * payment.amount / 100))
|
|
||||||
for target in targets
|
|
||||||
]
|
|
||||||
transfers = [(wallet, amount) for wallet, amount in transfers if amount > 0]
|
|
||||||
amount_left = payment.amount - sum([amount for _, amount in transfers])
|
|
||||||
|
|
||||||
if amount_left < 0:
|
if total_percent > 100:
|
||||||
logger.error(
|
logger.error("splitpayment failure: total percent adds up to more than 100%")
|
||||||
"splitpayments failure: amount_left is negative.", payment.payment_hash
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# mark the original payment with one extra key, "splitted"
|
logger.debug(f"performing split payments to {len(targets)} targets")
|
||||||
# (this prevents us from doing this process again and it's informative)
|
for target in targets:
|
||||||
# and reduce it by the amount we're going to send to the producer
|
amount = int(payment.amount * target.percent / 100) # msats
|
||||||
await core_db.execute(
|
payment_hash, payment_request = await create_invoice(
|
||||||
"""
|
wallet_id=target.wallet,
|
||||||
UPDATE apipayments
|
amount=int(amount / 1000), # sats
|
||||||
SET extra = ?, amount = ?
|
internal=True,
|
||||||
WHERE hash = ?
|
memo=f"split payment: {target.percent}% for {target.alias or target.wallet}",
|
||||||
AND checking_id NOT LIKE 'internal_%'
|
|
||||||
""",
|
|
||||||
(
|
|
||||||
json.dumps(dict(**payment.extra, splitted=True)),
|
|
||||||
amount_left,
|
|
||||||
payment.payment_hash,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
# perform the internal transfer using the same payment_hash
|
|
||||||
for wallet, amount in transfers:
|
|
||||||
internal_checking_id = f"internal_{urlsafe_short_hash()}"
|
|
||||||
await create_payment(
|
|
||||||
wallet_id=wallet,
|
|
||||||
checking_id=internal_checking_id,
|
|
||||||
payment_request="",
|
|
||||||
payment_hash=payment.payment_hash,
|
|
||||||
amount=amount,
|
|
||||||
memo=payment.memo,
|
|
||||||
pending=False,
|
|
||||||
extra={"tag": "splitpayments"},
|
extra={"tag": "splitpayments"},
|
||||||
)
|
)
|
||||||
|
logger.debug(f"created split invoice: {payment_hash}")
|
||||||
|
|
||||||
# manually send this for now
|
checking_id = await pay_invoice(
|
||||||
await internal_invoice_queue.put(internal_checking_id)
|
payment_request=payment_request,
|
||||||
return
|
wallet_id=payment.wallet_id,
|
||||||
|
extra={"tag": "splitpayments"},
|
||||||
|
)
|
||||||
|
logger.debug(f"paid split invoice: {checking_id}")
|
||||||
|
|
|
||||||
|
|
@ -31,14 +31,20 @@
|
||||||
style="flex-wrap: nowrap"
|
style="flex-wrap: nowrap"
|
||||||
v-for="(target, t) in targets"
|
v-for="(target, t) in targets"
|
||||||
>
|
>
|
||||||
<q-input
|
<q-select
|
||||||
dense
|
dense
|
||||||
outlined
|
:options="g.user.wallets.filter(w => w.id !== selectedWallet.id).map(o => ({name: o.name, value: o.id}))"
|
||||||
v-model="target.wallet"
|
v-model="target.wallet"
|
||||||
label="Wallet"
|
label="Wallet"
|
||||||
:hint="t === targets.length - 1 ? 'A wallet ID or invoice key.' : undefined"
|
:hint="t === targets.length - 1 ? 'A wallet ID or invoice key.' : undefined"
|
||||||
@input="targetChanged(false)"
|
@input="targetChanged(false)"
|
||||||
></q-input>
|
option-label="name"
|
||||||
|
style="width: 1000px"
|
||||||
|
new-value-mode="add-unique"
|
||||||
|
use-input
|
||||||
|
input-debounce="0"
|
||||||
|
emit-value
|
||||||
|
></q-select>
|
||||||
<q-input
|
<q-input
|
||||||
dense
|
dense
|
||||||
outlined
|
outlined
|
||||||
|
|
|
||||||
|
|
@ -245,7 +245,7 @@ async def api_delete_donation(donation_id, g: WalletTypeInfo = Depends(get_key_t
|
||||||
detail="Not authorized to delete this donation!",
|
detail="Not authorized to delete this donation!",
|
||||||
)
|
)
|
||||||
await delete_donation(donation_id)
|
await delete_donation(donation_id)
|
||||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
return "", HTTPStatus.NO_CONTENT
|
||||||
|
|
||||||
|
|
||||||
@streamalerts_ext.delete("/api/v1/services/{service_id}")
|
@streamalerts_ext.delete("/api/v1/services/{service_id}")
|
||||||
|
|
@ -262,4 +262,4 @@ async def api_delete_service(service_id, g: WalletTypeInfo = Depends(get_key_typ
|
||||||
detail="Not authorized to delete this service!",
|
detail="Not authorized to delete this service!",
|
||||||
)
|
)
|
||||||
await delete_service(service_id)
|
await delete_service(service_id)
|
||||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
return "", HTTPStatus.NO_CONTENT
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,10 @@ from typing import List, Optional, Union
|
||||||
from lnbits.helpers import urlsafe_short_hash
|
from lnbits.helpers import urlsafe_short_hash
|
||||||
|
|
||||||
from . import db
|
from . import db
|
||||||
from .models import CreateDomain, Domains, Subdomains
|
from .models import CreateDomain, CreateSubdomain, Domains, Subdomains
|
||||||
|
|
||||||
|
|
||||||
async def create_subdomain(payment_hash, wallet, data: CreateDomain) -> Subdomains:
|
async def create_subdomain(payment_hash, wallet, data: CreateSubdomain) -> Subdomains:
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO subdomains.subdomain (id, domain, email, subdomain, ip, wallet, sats, duration, paid, record_type)
|
INSERT INTO subdomains.subdomain (id, domain, email, subdomain, ip, wallet, sats, duration, paid, record_type)
|
||||||
|
|
|
||||||
|
|
@ -3,24 +3,24 @@ from pydantic.main import BaseModel
|
||||||
|
|
||||||
|
|
||||||
class CreateDomain(BaseModel):
|
class CreateDomain(BaseModel):
|
||||||
wallet: str = Query(...)
|
wallet: str = Query(...) # type: ignore
|
||||||
domain: str = Query(...)
|
domain: str = Query(...) # type: ignore
|
||||||
cf_token: str = Query(...)
|
cf_token: str = Query(...) # type: ignore
|
||||||
cf_zone_id: str = Query(...)
|
cf_zone_id: str = Query(...) # type: ignore
|
||||||
webhook: str = Query("")
|
webhook: str = Query("") # type: ignore
|
||||||
description: str = Query(..., min_length=0)
|
description: str = Query(..., min_length=0) # type: ignore
|
||||||
cost: int = Query(..., ge=0)
|
cost: int = Query(..., ge=0) # type: ignore
|
||||||
allowed_record_types: str = Query(...)
|
allowed_record_types: str = Query(...) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
class CreateSubdomain(BaseModel):
|
class CreateSubdomain(BaseModel):
|
||||||
domain: str = Query(...)
|
domain: str = Query(...) # type: ignore
|
||||||
subdomain: str = Query(...)
|
subdomain: str = Query(...) # type: ignore
|
||||||
email: str = Query(...)
|
email: str = Query(...) # type: ignore
|
||||||
ip: str = Query(...)
|
ip: str = Query(...) # type: ignore
|
||||||
sats: int = Query(..., ge=0)
|
sats: int = Query(..., ge=0) # type: ignore
|
||||||
duration: int = Query(...)
|
duration: int = Query(...) # type: ignore
|
||||||
record_type: str = Query(...)
|
record_type: str = Query(...) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
class Domains(BaseModel):
|
class Domains(BaseModel):
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ async def wait_for_paid_invoices():
|
||||||
|
|
||||||
|
|
||||||
async def on_invoice_paid(payment: Payment) -> None:
|
async def on_invoice_paid(payment: Payment) -> None:
|
||||||
if payment.extra.get("tag") != "lnsubdomain":
|
if not payment.extra or payment.extra.get("tag") != "lnsubdomain":
|
||||||
# not an lnurlp invoice
|
# not an lnurlp invoice
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
@ -37,7 +37,7 @@ async def on_invoice_paid(payment: Payment) -> None:
|
||||||
)
|
)
|
||||||
|
|
||||||
### Use webhook to notify about cloudflare registration
|
### Use webhook to notify about cloudflare registration
|
||||||
if domain.webhook:
|
if domain and domain.webhook:
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
try:
|
try:
|
||||||
r = await client.post(
|
r = await client.post(
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,9 @@ templates = Jinja2Templates(directory="templates")
|
||||||
|
|
||||||
|
|
||||||
@subdomains_ext.get("/", response_class=HTMLResponse)
|
@subdomains_ext.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) # type:ignore
|
||||||
|
):
|
||||||
return subdomains_renderer().TemplateResponse(
|
return subdomains_renderer().TemplateResponse(
|
||||||
"subdomains/index.html", {"request": request, "user": user.dict()}
|
"subdomains/index.html", {"request": request, "user": user.dict()}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -29,12 +29,15 @@ from .crud import (
|
||||||
|
|
||||||
@subdomains_ext.get("/api/v1/domains")
|
@subdomains_ext.get("/api/v1/domains")
|
||||||
async def api_domains(
|
async def api_domains(
|
||||||
g: WalletTypeInfo = Depends(get_key_type), all_wallets: bool = Query(False)
|
g: WalletTypeInfo = Depends(get_key_type), # type: ignore
|
||||||
|
all_wallets: bool = Query(False),
|
||||||
):
|
):
|
||||||
wallet_ids = [g.wallet.id]
|
wallet_ids = [g.wallet.id]
|
||||||
|
|
||||||
if all_wallets:
|
if all_wallets:
|
||||||
wallet_ids = (await get_user(g.wallet.user)).wallet_ids
|
user = await get_user(g.wallet.user)
|
||||||
|
if user is not None:
|
||||||
|
wallet_ids = user.wallet_ids
|
||||||
|
|
||||||
return [domain.dict() for domain in await get_domains(wallet_ids)]
|
return [domain.dict() for domain in await get_domains(wallet_ids)]
|
||||||
|
|
||||||
|
|
@ -42,7 +45,9 @@ async def api_domains(
|
||||||
@subdomains_ext.post("/api/v1/domains")
|
@subdomains_ext.post("/api/v1/domains")
|
||||||
@subdomains_ext.put("/api/v1/domains/{domain_id}")
|
@subdomains_ext.put("/api/v1/domains/{domain_id}")
|
||||||
async def api_domain_create(
|
async def api_domain_create(
|
||||||
data: CreateDomain, domain_id=None, g: WalletTypeInfo = Depends(get_key_type)
|
data: CreateDomain,
|
||||||
|
domain_id=None,
|
||||||
|
g: WalletTypeInfo = Depends(get_key_type), # type: ignore
|
||||||
):
|
):
|
||||||
if domain_id:
|
if domain_id:
|
||||||
domain = await get_domain(domain_id)
|
domain = await get_domain(domain_id)
|
||||||
|
|
@ -63,7 +68,9 @@ async def api_domain_create(
|
||||||
|
|
||||||
|
|
||||||
@subdomains_ext.delete("/api/v1/domains/{domain_id}")
|
@subdomains_ext.delete("/api/v1/domains/{domain_id}")
|
||||||
async def api_domain_delete(domain_id, g: WalletTypeInfo = Depends(get_key_type)):
|
async def api_domain_delete(
|
||||||
|
domain_id, g: WalletTypeInfo = Depends(get_key_type) # type: ignore
|
||||||
|
):
|
||||||
domain = await get_domain(domain_id)
|
domain = await get_domain(domain_id)
|
||||||
|
|
||||||
if not domain:
|
if not domain:
|
||||||
|
|
@ -74,7 +81,7 @@ async def api_domain_delete(domain_id, g: WalletTypeInfo = Depends(get_key_type)
|
||||||
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not your domain.")
|
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not your domain.")
|
||||||
|
|
||||||
await delete_domain(domain_id)
|
await delete_domain(domain_id)
|
||||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
return "", HTTPStatus.NO_CONTENT
|
||||||
|
|
||||||
|
|
||||||
#########subdomains##########
|
#########subdomains##########
|
||||||
|
|
@ -82,12 +89,14 @@ async def api_domain_delete(domain_id, g: WalletTypeInfo = Depends(get_key_type)
|
||||||
|
|
||||||
@subdomains_ext.get("/api/v1/subdomains")
|
@subdomains_ext.get("/api/v1/subdomains")
|
||||||
async def api_subdomains(
|
async def api_subdomains(
|
||||||
all_wallets: bool = Query(False), g: WalletTypeInfo = Depends(get_key_type)
|
all_wallets: bool = Query(False), g: WalletTypeInfo = Depends(get_key_type) # type: ignore
|
||||||
):
|
):
|
||||||
wallet_ids = [g.wallet.id]
|
wallet_ids = [g.wallet.id]
|
||||||
|
|
||||||
if all_wallets:
|
if all_wallets:
|
||||||
wallet_ids = (await get_user(g.wallet.user)).wallet_ids
|
user = await get_user(g.wallet.user)
|
||||||
|
if user is not None:
|
||||||
|
wallet_ids = user.wallet_ids
|
||||||
|
|
||||||
return [domain.dict() for domain in await get_subdomains(wallet_ids)]
|
return [domain.dict() for domain in await get_subdomains(wallet_ids)]
|
||||||
|
|
||||||
|
|
@ -173,7 +182,9 @@ async def api_subdomain_send_subdomain(payment_hash):
|
||||||
|
|
||||||
|
|
||||||
@subdomains_ext.delete("/api/v1/subdomains/{subdomain_id}")
|
@subdomains_ext.delete("/api/v1/subdomains/{subdomain_id}")
|
||||||
async def api_subdomain_delete(subdomain_id, g: WalletTypeInfo = Depends(get_key_type)):
|
async def api_subdomain_delete(
|
||||||
|
subdomain_id, g: WalletTypeInfo = Depends(get_key_type) # type: ignore
|
||||||
|
):
|
||||||
subdomain = await get_subdomain(subdomain_id)
|
subdomain = await get_subdomain(subdomain_id)
|
||||||
|
|
||||||
if not subdomain:
|
if not subdomain:
|
||||||
|
|
@ -187,4 +198,4 @@ async def api_subdomain_delete(subdomain_id, g: WalletTypeInfo = Depends(get_key
|
||||||
)
|
)
|
||||||
|
|
||||||
await delete_subdomain(subdomain_id)
|
await delete_subdomain(subdomain_id)
|
||||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
return "", HTTPStatus.NO_CONTENT
|
||||||
|
|
|
||||||
|
|
@ -11,5 +11,5 @@ An easy, fast and secure way to accept Bitcoin, over Lightning Network, at your
|
||||||

|

|
||||||
3. Open TPOS on the browser\
|
3. Open TPOS on the browser\
|
||||||

|

|
||||||
4. Present invoice QR to costumer\
|
4. Present invoice QR to customer\
|
||||||

|

|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
import json
|
|
||||||
|
|
||||||
from lnbits.core import db as core_db
|
from loguru import logger
|
||||||
from lnbits.core.crud import create_payment
|
|
||||||
from lnbits.core.models import Payment
|
from lnbits.core.models import Payment
|
||||||
from lnbits.helpers import get_current_extension_name, urlsafe_short_hash
|
from lnbits.core.services import create_invoice, pay_invoice
|
||||||
from lnbits.tasks import internal_invoice_queue, register_invoice_listener
|
from lnbits.helpers import get_current_extension_name
|
||||||
|
from lnbits.tasks import register_invoice_listener
|
||||||
|
|
||||||
from .crud import get_tpos
|
from .crud import get_tpos
|
||||||
|
|
||||||
|
|
@ -20,11 +20,9 @@ async def wait_for_paid_invoices():
|
||||||
|
|
||||||
|
|
||||||
async def on_invoice_paid(payment: Payment) -> None:
|
async def on_invoice_paid(payment: Payment) -> None:
|
||||||
if payment.extra.get("tag") == "tpos" and payment.extra.get("tipSplitted"):
|
if payment.extra.get("tag") != "tpos":
|
||||||
# already splitted, ignore
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# now we make some special internal transfers (from no one to the receiver)
|
|
||||||
tpos = await get_tpos(payment.extra.get("tposId"))
|
tpos = await get_tpos(payment.extra.get("tposId"))
|
||||||
tipAmount = payment.extra.get("tipAmount")
|
tipAmount = payment.extra.get("tipAmount")
|
||||||
|
|
||||||
|
|
@ -32,39 +30,17 @@ async def on_invoice_paid(payment: Payment) -> None:
|
||||||
# no tip amount
|
# no tip amount
|
||||||
return
|
return
|
||||||
|
|
||||||
tipAmount = tipAmount * 1000
|
payment_hash, payment_request = await create_invoice(
|
||||||
amount = payment.amount - tipAmount
|
|
||||||
|
|
||||||
# mark the original payment with one extra key, "splitted"
|
|
||||||
# (this prevents us from doing this process again and it's informative)
|
|
||||||
# and reduce it by the amount we're going to send to the producer
|
|
||||||
await core_db.execute(
|
|
||||||
"""
|
|
||||||
UPDATE apipayments
|
|
||||||
SET extra = ?, amount = ?
|
|
||||||
WHERE hash = ?
|
|
||||||
AND checking_id NOT LIKE 'internal_%'
|
|
||||||
""",
|
|
||||||
(
|
|
||||||
json.dumps(dict(**payment.extra, tipSplitted=True)),
|
|
||||||
amount,
|
|
||||||
payment.payment_hash,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
# perform the internal transfer using the same payment_hash
|
|
||||||
internal_checking_id = f"internal_{urlsafe_short_hash()}"
|
|
||||||
await create_payment(
|
|
||||||
wallet_id=tpos.tip_wallet,
|
wallet_id=tpos.tip_wallet,
|
||||||
checking_id=internal_checking_id,
|
amount=int(tipAmount), # sats
|
||||||
payment_request="",
|
internal=True,
|
||||||
payment_hash=payment.payment_hash,
|
memo=f"tpos tip",
|
||||||
amount=tipAmount,
|
|
||||||
memo=f"Tip for {payment.memo}",
|
|
||||||
pending=False,
|
|
||||||
extra={"tipSplitted": True},
|
|
||||||
)
|
)
|
||||||
|
logger.debug(f"tpos: tip invoice created: {payment_hash}")
|
||||||
|
|
||||||
# manually send this for now
|
checking_id = await pay_invoice(
|
||||||
await internal_invoice_queue.put(internal_checking_id)
|
payment_request=payment_request,
|
||||||
return
|
wallet_id=payment.wallet_id,
|
||||||
|
extra={"tag": "tpos"},
|
||||||
|
)
|
||||||
|
logger.debug(f"tpos: tip invoice paid: {checking_id}")
|
||||||
|
|
|
||||||
|
|
@ -139,8 +139,12 @@
|
||||||
input-debounce="0"
|
input-debounce="0"
|
||||||
new-value-mode="add-unique"
|
new-value-mode="add-unique"
|
||||||
label="Tip % Options (hit enter to add values)"
|
label="Tip % Options (hit enter to add values)"
|
||||||
><q-tooltip>Hit enter to add values</q-tooltip></q-select
|
><q-tooltip>Hit enter to add values</q-tooltip>
|
||||||
>
|
<template v-slot:hint>
|
||||||
|
You can leave this blank. A default rounding option is available
|
||||||
|
(round amount to a value)
|
||||||
|
</template>
|
||||||
|
</q-select>
|
||||||
<div class="row q-mt-lg">
|
<div class="row q-mt-lg">
|
||||||
<q-btn
|
<q-btn
|
||||||
unelevated
|
unelevated
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
<q-page-sticky v-if="exchangeRate" expand position="top">
|
<q-page-sticky v-if="exchangeRate" expand position="top">
|
||||||
<div class="row justify-center full-width">
|
<div class="row justify-center full-width">
|
||||||
<div class="col-12 col-sm-8 col-md-6 col-lg-4 text-center">
|
<div class="col-12 col-sm-8 col-md-6 col-lg-4 text-center">
|
||||||
<h3 class="q-mb-md">{% raw %}{{ famount }}{% endraw %}</h3>
|
<h3 class="q-mb-md">{% raw %}{{ amountFormatted }}{% endraw %}</h3>
|
||||||
<h5 class="q-mt-none q-mb-sm">
|
<h5 class="q-mt-none q-mb-sm">
|
||||||
{% raw %}{{ fsat }}{% endraw %} <small>sat</small>
|
{% raw %}{{ fsat }}{% endraw %} <small>sat</small>
|
||||||
</h5>
|
</h5>
|
||||||
|
|
@ -148,6 +148,14 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</q-page-sticky>
|
</q-page-sticky>
|
||||||
|
<q-page-sticky position="top-right" :offset="[18, 18]">
|
||||||
|
<q-btn
|
||||||
|
@click="showLastPayments"
|
||||||
|
fab
|
||||||
|
icon="receipt_long"
|
||||||
|
color="primary"
|
||||||
|
/>
|
||||||
|
</q-page-sticky>
|
||||||
<q-dialog
|
<q-dialog
|
||||||
v-model="invoiceDialog.show"
|
v-model="invoiceDialog.show"
|
||||||
position="top"
|
position="top"
|
||||||
|
|
@ -165,12 +173,14 @@
|
||||||
></qrcode>
|
></qrcode>
|
||||||
</q-responsive>
|
</q-responsive>
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<h3 class="q-my-md">{% raw %}{{ famount }}{% endraw %}</h3>
|
<h3 class="q-my-md">
|
||||||
|
{% raw %}{{ amountWithTipFormatted }}{% endraw %}
|
||||||
|
</h3>
|
||||||
<h5 class="q-mt-none">
|
<h5 class="q-mt-none">
|
||||||
{% raw %}{{ fsat }}
|
{% raw %}{{ fsat }}
|
||||||
<small>sat</small>
|
<small>sat</small>
|
||||||
<span v-show="tip_options" style="font-size: 0.75rem"
|
<span v-show="tip_options" style="font-size: 0.75rem"
|
||||||
>( + {{ tipAmountSat }} tip)</span
|
>( + {{ tipAmountFormatted }} tip)</span
|
||||||
>
|
>
|
||||||
{% endraw %}
|
{% endraw %}
|
||||||
</h5>
|
</h5>
|
||||||
|
|
@ -204,19 +214,48 @@
|
||||||
style="padding: 10px; margin: 3px"
|
style="padding: 10px; margin: 3px"
|
||||||
unelevated
|
unelevated
|
||||||
@click="processTipSelection(tip)"
|
@click="processTipSelection(tip)"
|
||||||
size="xl"
|
size="lg"
|
||||||
:outline="!($q.dark.isActive)"
|
:outline="!($q.dark.isActive)"
|
||||||
rounded
|
rounded
|
||||||
color="primary"
|
color="primary"
|
||||||
v-for="tip in this.tip_options"
|
v-for="tip in tip_options.filter(f => f != 'Round')"
|
||||||
:key="tip"
|
:key="tip"
|
||||||
>{% raw %}{{ tip }}{% endraw %}%</q-btn
|
>{% raw %}{{ tip }}{% endraw %}%</q-btn
|
||||||
>
|
>
|
||||||
|
<q-btn
|
||||||
|
style="padding: 10px; margin: 3px"
|
||||||
|
unelevated
|
||||||
|
@click="setRounding"
|
||||||
|
size="lg"
|
||||||
|
:outline="!($q.dark.isActive)"
|
||||||
|
rounded
|
||||||
|
color="primary"
|
||||||
|
label="Round to"
|
||||||
|
></q-btn>
|
||||||
|
<div class="row q-my-lg" v-if="rounding">
|
||||||
|
<q-input
|
||||||
|
class="col"
|
||||||
|
ref="inputRounding"
|
||||||
|
v-model.number="tipRounding"
|
||||||
|
:placeholder="roundToSugestion"
|
||||||
|
type="number"
|
||||||
|
hint="Total amount including tip"
|
||||||
|
:prefix="currency"
|
||||||
|
>
|
||||||
|
</q-input>
|
||||||
|
<q-btn
|
||||||
|
class="q-ml-sm"
|
||||||
|
style="margin-bottom: 20px"
|
||||||
|
color="primary"
|
||||||
|
@click="calculatePercent"
|
||||||
|
>Ok</q-btn
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-center q-mb-xl">
|
|
||||||
<p><a @click="processTipSelection(0)"> No, thanks</a></p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="row q-mt-lg">
|
<div class="row q-mt-lg">
|
||||||
|
<q-btn flat color="primary" @click="processTipSelection(0)"
|
||||||
|
>No, thanks</q-btn
|
||||||
|
>
|
||||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
|
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
|
||||||
</div>
|
</div>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
|
@ -256,6 +295,38 @@
|
||||||
style="font-size: min(90vw, 40em)"
|
style="font-size: min(90vw, 40em)"
|
||||||
></q-icon>
|
></q-icon>
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
|
|
||||||
|
<q-dialog v-model="lastPaymentsDialog.show" position="bottom">
|
||||||
|
<q-card class="lnbits__dialog-card">
|
||||||
|
<q-card-section class="row items-center q-pb-none">
|
||||||
|
<q-space />
|
||||||
|
<q-btn icon="close" flat round dense v-close-popup />
|
||||||
|
</q-card-section>
|
||||||
|
<q-list separator class="q-mb-lg">
|
||||||
|
<q-item v-if="!lastPaymentsDialog.data.length">
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label class="text-bold">No paid invoices</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item v-for="(payment, idx) in lastPaymentsDialog.data" :key="idx">
|
||||||
|
{%raw%}
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label class="text-bold"
|
||||||
|
>{{payment.amount / 1000}} sats</q-item-label
|
||||||
|
>
|
||||||
|
<q-item-label caption lines="2"
|
||||||
|
>Hash: {{payment.checking_id.slice(0, 30)}}...</q-item-label
|
||||||
|
>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section side top>
|
||||||
|
<q-item-label caption>{{payment.dateFrom}}</q-item-label>
|
||||||
|
<q-icon name="check" color="green" />
|
||||||
|
</q-item-section>
|
||||||
|
{%endraw%}
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
</q-page>
|
</q-page>
|
||||||
</q-page-container>
|
</q-page-container>
|
||||||
{% endblock %} {% block styles %}
|
{% endblock %} {% block styles %}
|
||||||
|
|
@ -294,8 +365,13 @@
|
||||||
exchangeRate: null,
|
exchangeRate: null,
|
||||||
stack: [],
|
stack: [],
|
||||||
tipAmount: 0.0,
|
tipAmount: 0.0,
|
||||||
|
tipRounding: null,
|
||||||
hasNFC: false,
|
hasNFC: false,
|
||||||
nfcTagReading: false,
|
nfcTagReading: false,
|
||||||
|
lastPaymentsDialog: {
|
||||||
|
show: false,
|
||||||
|
data: []
|
||||||
|
},
|
||||||
invoiceDialog: {
|
invoiceDialog: {
|
||||||
show: false,
|
show: false,
|
||||||
data: null,
|
data: null,
|
||||||
|
|
@ -310,32 +386,81 @@
|
||||||
},
|
},
|
||||||
complete: {
|
complete: {
|
||||||
show: false
|
show: false
|
||||||
}
|
},
|
||||||
|
rounding: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
amount: function () {
|
amount: function () {
|
||||||
if (!this.stack.length) return 0.0
|
if (!this.stack.length) return 0.0
|
||||||
return (Number(this.stack.join('')) / 100).toFixed(2)
|
return Number(this.stack.join('') / 100)
|
||||||
},
|
},
|
||||||
famount: function () {
|
amountFormatted: function () {
|
||||||
return LNbits.utils.formatCurrency(this.amount, this.currency)
|
return LNbits.utils.formatCurrency(
|
||||||
|
this.amount.toFixed(2),
|
||||||
|
this.currency
|
||||||
|
)
|
||||||
|
},
|
||||||
|
amountWithTipFormatted: function () {
|
||||||
|
return LNbits.utils.formatCurrency(
|
||||||
|
(this.amount + this.tipAmount).toFixed(2),
|
||||||
|
this.currency
|
||||||
|
)
|
||||||
},
|
},
|
||||||
sat: function () {
|
sat: function () {
|
||||||
if (!this.exchangeRate) return 0
|
if (!this.exchangeRate) return 0
|
||||||
return Math.ceil(
|
return Math.ceil((this.amount / this.exchangeRate) * 100000000)
|
||||||
((this.amount - this.tipAmount) / this.exchangeRate) * 100000000
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
tipAmountSat: function () {
|
tipAmountSat: function () {
|
||||||
if (!this.exchangeRate) return 0
|
if (!this.exchangeRate) return 0
|
||||||
return Math.ceil((this.tipAmount / this.exchangeRate) * 100000000)
|
return Math.ceil((this.tipAmount / this.exchangeRate) * 100000000)
|
||||||
},
|
},
|
||||||
|
tipAmountFormatted: function () {
|
||||||
|
return LNbits.utils.formatSat(this.tipAmountSat)
|
||||||
|
},
|
||||||
fsat: function () {
|
fsat: function () {
|
||||||
return LNbits.utils.formatSat(this.sat)
|
return LNbits.utils.formatSat(this.sat)
|
||||||
|
},
|
||||||
|
isRoundValid() {
|
||||||
|
return this.tipRounding > this.amount
|
||||||
|
},
|
||||||
|
roundToSugestion() {
|
||||||
|
switch (true) {
|
||||||
|
case this.amount > 50:
|
||||||
|
toNext = 10
|
||||||
|
break
|
||||||
|
case this.amount > 6:
|
||||||
|
toNext = 5
|
||||||
|
break
|
||||||
|
case this.amount > 2.5:
|
||||||
|
toNext = 1
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
toNext = 0.5
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.ceil(this.amount / toNext) * toNext
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
setRounding() {
|
||||||
|
this.rounding = true
|
||||||
|
this.tipRounding = this.roundToSugestion
|
||||||
|
this.$nextTick(() => this.$refs.inputRounding.focus())
|
||||||
|
},
|
||||||
|
calculatePercent() {
|
||||||
|
let change = ((this.tipRounding - this.amount) / this.amount) * 100
|
||||||
|
if (change < 0) {
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'warning',
|
||||||
|
message: 'Amount with tip must be greater than initial amount.'
|
||||||
|
})
|
||||||
|
this.tipRounding = this.roundToSugestion
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.processTipSelection(change)
|
||||||
|
},
|
||||||
closeInvoiceDialog: function () {
|
closeInvoiceDialog: function () {
|
||||||
this.stack = []
|
this.stack = []
|
||||||
this.tipAmount = 0.0
|
this.tipAmount = 0.0
|
||||||
|
|
@ -348,30 +473,18 @@
|
||||||
processTipSelection: function (selectedTipOption) {
|
processTipSelection: function (selectedTipOption) {
|
||||||
this.tipDialog.show = false
|
this.tipDialog.show = false
|
||||||
|
|
||||||
if (selectedTipOption) {
|
if (!selectedTipOption) {
|
||||||
const tipAmount = parseFloat(
|
this.tipAmount = 0.0
|
||||||
parseFloat((selectedTipOption / 100) * this.amount)
|
return this.showInvoice()
|
||||||
)
|
|
||||||
const subtotal = parseFloat(this.amount)
|
|
||||||
const grandTotal = parseFloat((tipAmount + subtotal).toFixed(2))
|
|
||||||
const totalString = grandTotal.toFixed(2).toString()
|
|
||||||
|
|
||||||
this.stack = []
|
|
||||||
for (var i = 0; i < totalString.length; i++) {
|
|
||||||
const char = totalString[i]
|
|
||||||
|
|
||||||
if (char !== '.') {
|
|
||||||
this.stack.push(char)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.tipAmount = tipAmount
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.tipAmount = (selectedTipOption / 100) * this.amount
|
||||||
this.showInvoice()
|
this.showInvoice()
|
||||||
},
|
},
|
||||||
submitForm: function () {
|
submitForm: function () {
|
||||||
if (this.tip_options && this.tip_options.length) {
|
if (this.tip_options && this.tip_options.length) {
|
||||||
|
this.rounding = false
|
||||||
|
this.tipRounding = null
|
||||||
this.showTipModal()
|
this.showTipModal()
|
||||||
} else {
|
} else {
|
||||||
this.showInvoice()
|
this.showInvoice()
|
||||||
|
|
@ -520,6 +633,24 @@
|
||||||
self.exchangeRate =
|
self.exchangeRate =
|
||||||
response.data.data['BTC' + self.currency][self.currency]
|
response.data.data['BTC' + self.currency][self.currency]
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
getLastPayments() {
|
||||||
|
return axios
|
||||||
|
.get(`/tpos/api/v1/tposs/${this.tposId}/invoices`)
|
||||||
|
.then(res => {
|
||||||
|
if (res.data && res.data.length) {
|
||||||
|
let last = [...res.data]
|
||||||
|
this.lastPaymentsDialog.data = last.map(obj => {
|
||||||
|
obj.dateFrom = moment(obj.time * 1000).fromNow()
|
||||||
|
return obj
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(e => console.error(e))
|
||||||
|
},
|
||||||
|
showLastPayments() {
|
||||||
|
this.getLastPayments()
|
||||||
|
this.lastPaymentsDialog.show = true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created: function () {
|
created: function () {
|
||||||
|
|
@ -529,10 +660,26 @@
|
||||||
'{{ tpos.tip_options | tojson }}' == 'null'
|
'{{ tpos.tip_options | tojson }}' == 'null'
|
||||||
? null
|
? null
|
||||||
: JSON.parse('{{ tpos.tip_options }}')
|
: JSON.parse('{{ tpos.tip_options }}')
|
||||||
|
|
||||||
|
if ('{{ tpos.tip_wallet }}') {
|
||||||
|
this.tip_options.push('Round')
|
||||||
|
}
|
||||||
setInterval(function () {
|
setInterval(function () {
|
||||||
getRates()
|
getRates()
|
||||||
}, 120000)
|
}, 120000)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
input::-webkit-outer-spin-button,
|
||||||
|
input::-webkit-inner-spin-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Firefox */
|
||||||
|
input[type='number'] {
|
||||||
|
-moz-appearance: textfield;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,8 @@ from lnurl import decode as decode_lnurl
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from starlette.exceptions import HTTPException
|
from starlette.exceptions import HTTPException
|
||||||
|
|
||||||
from lnbits.core.crud import get_user
|
from lnbits.core.crud import get_latest_payments_by_extension, get_user
|
||||||
|
from lnbits.core.models import Payment
|
||||||
from lnbits.core.services import create_invoice
|
from lnbits.core.services import create_invoice
|
||||||
from lnbits.core.views.api import api_payment
|
from lnbits.core.views.api import api_payment
|
||||||
from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
|
from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
|
||||||
|
|
@ -51,7 +52,7 @@ async def api_tpos_delete(
|
||||||
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not your TPoS.")
|
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not your TPoS.")
|
||||||
|
|
||||||
await delete_tpos(tpos_id)
|
await delete_tpos(tpos_id)
|
||||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
return "", HTTPStatus.NO_CONTENT
|
||||||
|
|
||||||
|
|
||||||
@tpos_ext.post("/api/v1/tposs/{tpos_id}/invoices", status_code=HTTPStatus.CREATED)
|
@tpos_ext.post("/api/v1/tposs/{tpos_id}/invoices", status_code=HTTPStatus.CREATED)
|
||||||
|
|
@ -81,6 +82,30 @@ async def api_tpos_create_invoice(
|
||||||
return {"payment_hash": payment_hash, "payment_request": payment_request}
|
return {"payment_hash": payment_hash, "payment_request": payment_request}
|
||||||
|
|
||||||
|
|
||||||
|
@tpos_ext.get("/api/v1/tposs/{tpos_id}/invoices")
|
||||||
|
async def api_tpos_get_latest_invoices(tpos_id: str = None):
|
||||||
|
try:
|
||||||
|
payments = [
|
||||||
|
Payment.from_row(row)
|
||||||
|
for row in await get_latest_payments_by_extension(
|
||||||
|
ext_name="tpos", ext_id=tpos_id
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e))
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"checking_id": payment.checking_id,
|
||||||
|
"amount": payment.amount,
|
||||||
|
"time": payment.time,
|
||||||
|
"pending": payment.pending,
|
||||||
|
}
|
||||||
|
for payment in payments
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
@tpos_ext.post(
|
@tpos_ext.post(
|
||||||
"/api/v1/tposs/{tpos_id}/invoices/{payment_request}/pay", status_code=HTTPStatus.OK
|
"/api/v1/tposs/{tpos_id}/invoices/{payment_request}/pay", status_code=HTTPStatus.OK
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,8 @@ async def get_usermanager_users(user_id: str) -> List[Users]:
|
||||||
return [Users(**row) for row in rows]
|
return [Users(**row) for row in rows]
|
||||||
|
|
||||||
|
|
||||||
async def delete_usermanager_user(user_id: str) -> None:
|
async def delete_usermanager_user(user_id: str, delete_core: bool = True) -> None:
|
||||||
|
if delete_core:
|
||||||
wallets = await get_usermanager_wallets(user_id)
|
wallets = await get_usermanager_wallets(user_id)
|
||||||
for wallet in wallets:
|
for wallet in wallets:
|
||||||
await delete_wallet(user_id=user_id, wallet_id=wallet.id)
|
await delete_wallet(user_id=user_id, wallet_id=wallet.id)
|
||||||
|
|
|
||||||
|
|
@ -38,13 +38,13 @@
|
||||||
>
|
>
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||||
Returns 201 CREATED (application/json)
|
Returns 200 OK (application/json)
|
||||||
</h5>
|
</h5>
|
||||||
<code>JSON list of users</code>
|
<code>JSON list of users</code>
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||||
<code
|
<code
|
||||||
>curl -X GET {{ request.base_url }}usermanager/api/v1/users -H
|
>curl -X GET {{ request.base_url }}usermanager/api/v1/users -H
|
||||||
"X-Api-Key: {{ user.wallets[0].inkey }}"
|
"X-Api-Key: {{ user.wallets[0].adminkey }}"
|
||||||
</code>
|
</code>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
|
@ -57,10 +57,16 @@
|
||||||
/usermanager/api/v1/users/<user_id></code
|
/usermanager/api/v1/users/<user_id></code
|
||||||
>
|
>
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
||||||
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||||
Returns 201 CREATED (application/json)
|
Returns 200 OK (application/json)
|
||||||
</h5>
|
</h5>
|
||||||
<code>JSON list of users</code>
|
<code
|
||||||
|
>{"id": <string>, "name": <string>, "admin":
|
||||||
|
<string>, "email": <string>, "password":
|
||||||
|
<string>}</code
|
||||||
|
>
|
||||||
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||||
<code
|
<code
|
||||||
>curl -X GET {{ request.base_url
|
>curl -X GET {{ request.base_url
|
||||||
|
|
@ -75,20 +81,19 @@
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<code
|
<code
|
||||||
><span class="text-light-blue">GET</span>
|
><span class="text-light-blue">GET</span>
|
||||||
/usermanager/api/v1/wallets/<user_id></code
|
/usermanager/api/v1/wallets</code
|
||||||
>
|
>
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||||
<code>{"X-Api-Key": <string>}</code>
|
<code>{"X-Api-Key": <string>}</code>
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||||
Returns 201 CREATED (application/json)
|
Returns 200 OK (application/json)
|
||||||
</h5>
|
</h5>
|
||||||
<code>JSON wallet data</code>
|
<code>JSON wallet data</code>
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||||
<code
|
<code
|
||||||
>curl -X GET {{ request.base_url
|
>curl -X GET {{ request.base_url }}usermanager/api/v1/wallets -H
|
||||||
}}usermanager/api/v1/wallets/<user_id> -H "X-Api-Key: {{
|
"X-Api-Key: {{ user.wallets[0].adminkey }}"
|
||||||
user.wallets[0].inkey }}"
|
|
||||||
</code>
|
</code>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
|
@ -104,7 +109,7 @@
|
||||||
<code>{"X-Api-Key": <string>}</code>
|
<code>{"X-Api-Key": <string>}</code>
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||||
Returns 201 CREATED (application/json)
|
Returns 200 OK (application/json)
|
||||||
</h5>
|
</h5>
|
||||||
<code>JSON a wallets transactions</code>
|
<code>JSON a wallets transactions</code>
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||||
|
|
@ -215,7 +220,7 @@
|
||||||
<code
|
<code
|
||||||
>curl -X DELETE {{ request.base_url
|
>curl -X DELETE {{ request.base_url
|
||||||
}}usermanager/api/v1/users/<user_id> -H "X-Api-Key: {{
|
}}usermanager/api/v1/users/<user_id> -H "X-Api-Key: {{
|
||||||
user.wallets[0].inkey }}"
|
user.wallets[0].adminkey }}"
|
||||||
</code>
|
</code>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
|
@ -233,7 +238,7 @@
|
||||||
<code
|
<code
|
||||||
>curl -X DELETE {{ request.base_url
|
>curl -X DELETE {{ request.base_url
|
||||||
}}usermanager/api/v1/wallets/<wallet_id> -H "X-Api-Key: {{
|
}}usermanager/api/v1/wallets/<wallet_id> -H "X-Api-Key: {{
|
||||||
user.wallets[0].inkey }}"
|
user.wallets[0].adminkey }}"
|
||||||
</code>
|
</code>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
|
@ -254,11 +259,15 @@
|
||||||
<code>{"X-Api-Key": <string>}</code>
|
<code>{"X-Api-Key": <string>}</code>
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||||
<code
|
<code
|
||||||
>curl -X POST {{ request.base_url }}usermanager/api/v1/extensions -d
|
>curl -X POST {{ request.base_url
|
||||||
'{"userid": <string>, "extension": <string>, "active":
|
}}usermanager/api/v1/extensions?extension=withdraw&userid=user_id&active=true
|
||||||
<integer>}' -H "X-Api-Key: {{ user.wallets[0].inkey }}" -H
|
-H "X-Api-Key: {{ user.wallets[0].inkey }}" -H "Content-type:
|
||||||
"Content-type: application/json"
|
application/json"
|
||||||
</code>
|
</code>
|
||||||
|
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||||
|
Returns 200 OK (application/json)
|
||||||
|
</h5>
|
||||||
|
<code>{"extension": "updated"}</code>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
</q-expansion-item>
|
</q-expansion-item>
|
||||||
|
|
|
||||||
|
|
@ -52,15 +52,17 @@ async def api_usermanager_users_create(
|
||||||
|
|
||||||
@usermanager_ext.delete("/api/v1/users/{user_id}")
|
@usermanager_ext.delete("/api/v1/users/{user_id}")
|
||||||
async def api_usermanager_users_delete(
|
async def api_usermanager_users_delete(
|
||||||
user_id, wallet: WalletTypeInfo = Depends(require_admin_key)
|
user_id,
|
||||||
|
delete_core: bool = Query(True),
|
||||||
|
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||||
):
|
):
|
||||||
user = await get_usermanager_user(user_id)
|
user = await get_usermanager_user(user_id)
|
||||||
if not user:
|
if not user:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.NOT_FOUND, detail="User does not exist."
|
status_code=HTTPStatus.NOT_FOUND, detail="User does not exist."
|
||||||
)
|
)
|
||||||
await delete_usermanager_user(user_id)
|
await delete_usermanager_user(user_id, delete_core)
|
||||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
return "", HTTPStatus.NO_CONTENT
|
||||||
|
|
||||||
|
|
||||||
# Activate Extension
|
# Activate Extension
|
||||||
|
|
@ -124,4 +126,4 @@ async def api_usermanager_wallets_delete(
|
||||||
status_code=HTTPStatus.NOT_FOUND, detail="Wallet does not exist."
|
status_code=HTTPStatus.NOT_FOUND, detail="Wallet does not exist."
|
||||||
)
|
)
|
||||||
await delete_usermanager_wallet(wallet_id, get_wallet.user)
|
await delete_usermanager_wallet(wallet_id, get_wallet.user)
|
||||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
return "", HTTPStatus.NO_CONTENT
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ from .models import Address, Config, WalletAccount
|
||||||
##########################WALLETS####################
|
##########################WALLETS####################
|
||||||
|
|
||||||
|
|
||||||
async def create_watch_wallet(w: WalletAccount) -> WalletAccount:
|
async def create_watch_wallet(user: str, w: WalletAccount) -> WalletAccount:
|
||||||
wallet_id = urlsafe_short_hash()
|
wallet_id = urlsafe_short_hash()
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"""
|
"""
|
||||||
|
|
@ -30,7 +30,7 @@ async def create_watch_wallet(w: WalletAccount) -> WalletAccount:
|
||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
wallet_id,
|
wallet_id,
|
||||||
w.user,
|
user,
|
||||||
w.masterpub,
|
w.masterpub,
|
||||||
w.fingerprint,
|
w.fingerprint,
|
||||||
w.title,
|
w.title,
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@ class CreateWallet(BaseModel):
|
||||||
|
|
||||||
class WalletAccount(BaseModel):
|
class WalletAccount(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
user: str
|
|
||||||
masterpub: str
|
masterpub: str
|
||||||
fingerprint: str
|
fingerprint: str
|
||||||
title: str
|
title: str
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
filled
|
filled
|
||||||
dense
|
dense
|
||||||
v-model.number="feeRate"
|
v-model.number="feeRate"
|
||||||
|
step="any"
|
||||||
:rules="[val => !!val || 'Field is required']"
|
:rules="[val => !!val || 'Field is required']"
|
||||||
type="number"
|
type="number"
|
||||||
label="sats/vbyte"
|
label="sats/vbyte"
|
||||||
|
|
|
||||||
|
|
@ -86,7 +86,6 @@ async def api_wallet_create_or_update(
|
||||||
|
|
||||||
new_wallet = WalletAccount(
|
new_wallet = WalletAccount(
|
||||||
id="none",
|
id="none",
|
||||||
user=w.wallet.user,
|
|
||||||
masterpub=data.masterpub,
|
masterpub=data.masterpub,
|
||||||
fingerprint=descriptor.keys[0].fingerprint.hex(),
|
fingerprint=descriptor.keys[0].fingerprint.hex(),
|
||||||
type=descriptor.scriptpubkey_type(),
|
type=descriptor.scriptpubkey_type(),
|
||||||
|
|
@ -115,7 +114,7 @@ async def api_wallet_create_or_update(
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
wallet = await create_watch_wallet(new_wallet)
|
wallet = await create_watch_wallet(w.wallet.user, new_wallet)
|
||||||
|
|
||||||
await api_get_addresses(wallet.id, w)
|
await api_get_addresses(wallet.id, w)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ from fastapi import HTTPException
|
||||||
from fastapi.param_functions import Query
|
from fastapi.param_functions import Query
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from starlette.requests import Request
|
from starlette.requests import Request
|
||||||
from starlette.responses import HTMLResponse # type: ignore
|
from starlette.responses import HTMLResponse
|
||||||
|
|
||||||
from lnbits.core.services import pay_invoice
|
from lnbits.core.services import pay_invoice
|
||||||
|
|
||||||
|
|
@ -51,10 +51,24 @@ async def api_lnurl_response(request: Request, unique_hash):
|
||||||
# CALLBACK
|
# CALLBACK
|
||||||
|
|
||||||
|
|
||||||
@withdraw_ext.get("/api/v1/lnurl/cb/{unique_hash}", name="withdraw.api_lnurl_callback")
|
@withdraw_ext.get(
|
||||||
|
"/api/v1/lnurl/cb/{unique_hash}",
|
||||||
|
name="withdraw.api_lnurl_callback",
|
||||||
|
summary="lnurl withdraw callback",
|
||||||
|
description="""
|
||||||
|
This enpoints allows you to put unique_hash, k1
|
||||||
|
and a payment_request to get your payment_request paid.
|
||||||
|
""",
|
||||||
|
response_description="JSON with status",
|
||||||
|
responses={
|
||||||
|
200: {"description": "status: OK"},
|
||||||
|
400: {"description": "k1 is wrong or link open time or withdraw not working."},
|
||||||
|
404: {"description": "withdraw link not found."},
|
||||||
|
405: {"description": "withdraw link is spent."},
|
||||||
|
},
|
||||||
|
)
|
||||||
async def api_lnurl_callback(
|
async def api_lnurl_callback(
|
||||||
unique_hash,
|
unique_hash,
|
||||||
request: Request,
|
|
||||||
k1: str = Query(...),
|
k1: str = Query(...),
|
||||||
pr: str = Query(...),
|
pr: str = Query(...),
|
||||||
id_unique_hash=None,
|
id_unique_hash=None,
|
||||||
|
|
@ -63,22 +77,25 @@ async def api_lnurl_callback(
|
||||||
now = int(datetime.now().timestamp())
|
now = int(datetime.now().timestamp())
|
||||||
if not link:
|
if not link:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.NOT_FOUND, detail="LNURL-withdraw not found"
|
status_code=HTTPStatus.NOT_FOUND, detail="withdraw not found."
|
||||||
)
|
)
|
||||||
|
|
||||||
if link.is_spent:
|
if link.is_spent:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.NOT_FOUND, detail="Withdraw is spent."
|
status_code=HTTPStatus.METHOD_NOT_ALLOWED, detail="withdraw is spent."
|
||||||
)
|
)
|
||||||
|
|
||||||
if link.k1 != k1:
|
if link.k1 != k1:
|
||||||
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Bad request.")
|
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail="k1 is wrong.")
|
||||||
|
|
||||||
if now < link.open_time:
|
if now < link.open_time:
|
||||||
return {"status": "ERROR", "reason": f"Wait {link.open_time - now} seconds."}
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
|
detail=f"wait link open_time {link.open_time - now} seconds.",
|
||||||
|
)
|
||||||
|
|
||||||
usescsv = ""
|
usescsv = ""
|
||||||
try:
|
|
||||||
for x in range(1, link.uses - link.used):
|
for x in range(1, link.uses - link.used):
|
||||||
usecv = link.usescsv.split(",")
|
usecv = link.usescsv.split(",")
|
||||||
usescsv += "," + str(usecv[x])
|
usescsv += "," + str(usecv[x])
|
||||||
|
|
@ -95,7 +112,7 @@ async def api_lnurl_callback(
|
||||||
usescsv = ",".join(useslist)
|
usescsv = ",".join(useslist)
|
||||||
if not found:
|
if not found:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.NOT_FOUND, detail="LNURL-withdraw not found."
|
status_code=HTTPStatus.NOT_FOUND, detail="withdraw not found."
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
usescsv = usescsv[1:]
|
usescsv = usescsv[1:]
|
||||||
|
|
@ -106,6 +123,7 @@ async def api_lnurl_callback(
|
||||||
"usescsv": usecsvback,
|
"usescsv": usecsvback,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
changes = {
|
changes = {
|
||||||
"open_time": link.wait_time + now,
|
"open_time": link.wait_time + now,
|
||||||
"used": link.used + 1,
|
"used": link.used + 1,
|
||||||
|
|
@ -143,7 +161,9 @@ async def api_lnurl_callback(
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
await update_withdraw_link(link.id, **changesback)
|
await update_withdraw_link(link.id, **changesback)
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
return {"status": "ERROR", "reason": "Link not working"}
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.BAD_REQUEST, detail=f"withdraw not working. {str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# FOR LNURLs WHICH ARE UNIQUE
|
# FOR LNURLs WHICH ARE UNIQUE
|
||||||
|
|
|
||||||
|
|
@ -290,8 +290,12 @@ new Vue({
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
exportCSV: function () {
|
exportCSV() {
|
||||||
LNbits.utils.exportCSV(this.paywallsTable.columns, this.paywalls)
|
LNbits.utils.exportCSV(
|
||||||
|
this.withdrawLinksTable.columns,
|
||||||
|
this.withdrawLinks,
|
||||||
|
'withdraw-links'
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created: function () {
|
created: function () {
|
||||||
|
|
|
||||||
|
|
@ -167,6 +167,7 @@ def template_renderer(additional_folders: List = []) -> Jinja2Templates:
|
||||||
)
|
)
|
||||||
|
|
||||||
if settings.LNBITS_AD_SPACE:
|
if settings.LNBITS_AD_SPACE:
|
||||||
|
t.env.globals["AD_TITLE"] = settings.LNBITS_AD_SPACE_TITLE
|
||||||
t.env.globals["AD_SPACE"] = settings.LNBITS_AD_SPACE
|
t.env.globals["AD_SPACE"] = settings.LNBITS_AD_SPACE
|
||||||
t.env.globals["HIDE_API"] = settings.LNBITS_HIDE_API
|
t.env.globals["HIDE_API"] = settings.LNBITS_HIDE_API
|
||||||
t.env.globals["SITE_TITLE"] = settings.LNBITS_SITE_TITLE
|
t.env.globals["SITE_TITLE"] = settings.LNBITS_SITE_TITLE
|
||||||
|
|
|
||||||
|
|
@ -1,95 +0,0 @@
|
||||||
from functools import partial
|
|
||||||
from typing import Callable, List, Optional
|
|
||||||
from urllib.parse import urlparse
|
|
||||||
from urllib.request import parse_http_list as _parse_list_header
|
|
||||||
|
|
||||||
from quart import Request
|
|
||||||
from quart_trio.asgi import TrioASGIHTTPConnection
|
|
||||||
from werkzeug.datastructures import Headers
|
|
||||||
|
|
||||||
|
|
||||||
class ASGIProxyFix(TrioASGIHTTPConnection):
|
|
||||||
def _create_request_from_scope(self, send: Callable) -> Request:
|
|
||||||
headers = Headers()
|
|
||||||
headers["Remote-Addr"] = (self.scope.get("client") or ["<local>"])[0]
|
|
||||||
for name, value in self.scope["headers"]:
|
|
||||||
headers.add(name.decode("latin1").title(), value.decode("latin1"))
|
|
||||||
if self.scope["http_version"] < "1.1":
|
|
||||||
headers.setdefault("Host", self.app.config["SERVER_NAME"] or "")
|
|
||||||
|
|
||||||
path = self.scope["path"]
|
|
||||||
path = path if path[0] == "/" else urlparse(path).path
|
|
||||||
|
|
||||||
x_proto = self._get_real_value(1, headers.get("X-Forwarded-Proto"))
|
|
||||||
if x_proto:
|
|
||||||
self.scope["scheme"] = x_proto
|
|
||||||
|
|
||||||
x_host = self._get_real_value(1, headers.get("X-Forwarded-Host"))
|
|
||||||
if x_host:
|
|
||||||
headers["host"] = x_host.lower()
|
|
||||||
|
|
||||||
return self.app.request_class(
|
|
||||||
self.scope["method"],
|
|
||||||
self.scope["scheme"],
|
|
||||||
path,
|
|
||||||
self.scope["query_string"],
|
|
||||||
headers,
|
|
||||||
self.scope.get("root_path", ""),
|
|
||||||
self.scope["http_version"],
|
|
||||||
max_content_length=self.app.config["MAX_CONTENT_LENGTH"],
|
|
||||||
body_timeout=self.app.config["BODY_TIMEOUT"],
|
|
||||||
send_push_promise=partial(self._send_push_promise, send),
|
|
||||||
scope=self.scope,
|
|
||||||
)
|
|
||||||
|
|
||||||
def _get_real_value(self, trusted: int, value: Optional[str]) -> Optional[str]:
|
|
||||||
"""Get the real value from a list header based on the configured
|
|
||||||
number of trusted proxies.
|
|
||||||
:param trusted: Number of values to trust in the header.
|
|
||||||
:param value: Comma separated list header value to parse.
|
|
||||||
:return: The real value, or ``None`` if there are fewer values
|
|
||||||
than the number of trusted proxies.
|
|
||||||
.. versionchanged:: 1.0
|
|
||||||
Renamed from ``_get_trusted_comma``.
|
|
||||||
.. versionadded:: 0.15
|
|
||||||
"""
|
|
||||||
if not (trusted and value):
|
|
||||||
return None
|
|
||||||
|
|
||||||
values = self.parse_list_header(value)
|
|
||||||
if len(values) >= trusted:
|
|
||||||
return values[-trusted]
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
def parse_list_header(self, value: str) -> List[str]:
|
|
||||||
result = []
|
|
||||||
for item in _parse_list_header(value):
|
|
||||||
if item[:1] == item[-1:] == '"':
|
|
||||||
item = self.unquote_header_value(item[1:-1])
|
|
||||||
result.append(item)
|
|
||||||
return result
|
|
||||||
|
|
||||||
def unquote_header_value(self, value: str, is_filename: bool = False) -> str:
|
|
||||||
r"""Unquotes a header value. (Reversal of :func:`quote_header_value`).
|
|
||||||
This does not use the real unquoting but what browsers are actually
|
|
||||||
using for quoting.
|
|
||||||
.. versionadded:: 0.5
|
|
||||||
:param value: the header value to unquote.
|
|
||||||
:param is_filename: The value represents a filename or path.
|
|
||||||
"""
|
|
||||||
if value and value[0] == value[-1] == '"':
|
|
||||||
# this is not the real unquoting, but fixing this so that the
|
|
||||||
# RFC is met will result in bugs with internet explorer and
|
|
||||||
# probably some other browsers as well. IE for example is
|
|
||||||
# uploading files with "C:\foo\bar.txt" as filename
|
|
||||||
value = value[1:-1]
|
|
||||||
|
|
||||||
# if this is a filename and the starting characters look like
|
|
||||||
# a UNC path, then just return the value without quotes. Using the
|
|
||||||
# replace sequence below on a UNC path has the effect of turning
|
|
||||||
# the leading double slash into a single slash and then
|
|
||||||
# _fix_ie_filename() doesn't work correctly. See #458.
|
|
||||||
if not is_filename or value[:2] != "\\\\":
|
|
||||||
return value.replace("\\\\", "\\").replace('\\"', '"')
|
|
||||||
return value
|
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
import time
|
|
||||||
|
|
||||||
import click
|
import click
|
||||||
import uvicorn
|
import uvicorn
|
||||||
|
|
||||||
from lnbits.settings import HOST, PORT
|
from lnbits.settings import FORWARDED_ALLOW_IPS, HOST, PORT
|
||||||
|
|
||||||
|
|
||||||
@click.command(
|
@click.command(
|
||||||
|
|
@ -14,10 +12,20 @@ from lnbits.settings import HOST, PORT
|
||||||
)
|
)
|
||||||
@click.option("--port", default=PORT, help="Port to listen on")
|
@click.option("--port", default=PORT, help="Port to listen on")
|
||||||
@click.option("--host", default=HOST, help="Host to run LNBits on")
|
@click.option("--host", default=HOST, help="Host to run LNBits on")
|
||||||
|
@click.option(
|
||||||
|
"--forwarded-allow-ips", default=FORWARDED_ALLOW_IPS, help="Allowed proxy servers"
|
||||||
|
)
|
||||||
@click.option("--ssl-keyfile", default=None, help="Path to SSL keyfile")
|
@click.option("--ssl-keyfile", default=None, help="Path to SSL keyfile")
|
||||||
@click.option("--ssl-certfile", default=None, help="Path to SSL certificate")
|
@click.option("--ssl-certfile", default=None, help="Path to SSL certificate")
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def main(ctx, port: int, host: str, ssl_keyfile: str, ssl_certfile: str):
|
def main(
|
||||||
|
ctx,
|
||||||
|
port: int,
|
||||||
|
host: str,
|
||||||
|
forwarded_allow_ips: str,
|
||||||
|
ssl_keyfile: str,
|
||||||
|
ssl_certfile: str,
|
||||||
|
):
|
||||||
"""Launched with `poetry run lnbits` at root level"""
|
"""Launched with `poetry run lnbits` at root level"""
|
||||||
# this beautiful beast parses all command line arguments and passes them to the uvicorn server
|
# this beautiful beast parses all command line arguments and passes them to the uvicorn server
|
||||||
d = dict()
|
d = dict()
|
||||||
|
|
@ -37,6 +45,7 @@ def main(ctx, port: int, host: str, ssl_keyfile: str, ssl_certfile: str):
|
||||||
"lnbits.__main__:app",
|
"lnbits.__main__:app",
|
||||||
port=port,
|
port=port,
|
||||||
host=host,
|
host=host,
|
||||||
|
forwarded_allow_ips=forwarded_allow_ips,
|
||||||
ssl_keyfile=ssl_keyfile,
|
ssl_keyfile=ssl_keyfile,
|
||||||
ssl_certfile=ssl_certfile,
|
ssl_certfile=ssl_certfile,
|
||||||
**d
|
**d
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,8 @@ DEBUG = env.bool("DEBUG", default=False)
|
||||||
HOST = env.str("HOST", default="127.0.0.1")
|
HOST = env.str("HOST", default="127.0.0.1")
|
||||||
PORT = env.int("PORT", default=5000)
|
PORT = env.int("PORT", default=5000)
|
||||||
|
|
||||||
|
FORWARDED_ALLOW_IPS = env.str("FORWARDED_ALLOW_IPS", default="127.0.0.1")
|
||||||
|
|
||||||
LNBITS_PATH = path.dirname(path.realpath(__file__))
|
LNBITS_PATH = path.dirname(path.realpath(__file__))
|
||||||
LNBITS_DATA_FOLDER = env.str(
|
LNBITS_DATA_FOLDER = env.str(
|
||||||
"LNBITS_DATA_FOLDER", default=path.join(LNBITS_PATH, "data")
|
"LNBITS_DATA_FOLDER", default=path.join(LNBITS_PATH, "data")
|
||||||
|
|
@ -38,6 +40,9 @@ LNBITS_DISABLED_EXTENSIONS: List[str] = [
|
||||||
for x in env.list("LNBITS_DISABLED_EXTENSIONS", default=[], subcast=str)
|
for x in env.list("LNBITS_DISABLED_EXTENSIONS", default=[], subcast=str)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
LNBITS_AD_SPACE_TITLE = env.str(
|
||||||
|
"LNBITS_AD_SPACE_TITLE", default="Optional Advert Space"
|
||||||
|
)
|
||||||
LNBITS_AD_SPACE = [x.strip(" ") for x in env.list("LNBITS_AD_SPACE", default=[])]
|
LNBITS_AD_SPACE = [x.strip(" ") for x in env.list("LNBITS_AD_SPACE", default=[])]
|
||||||
LNBITS_HIDE_API = env.bool("LNBITS_HIDE_API", default=False)
|
LNBITS_HIDE_API = env.bool("LNBITS_HIDE_API", default=False)
|
||||||
LNBITS_SITE_TITLE = env.str("LNBITS_SITE_TITLE", default="LNbits")
|
LNBITS_SITE_TITLE = env.str("LNBITS_SITE_TITLE", default="LNbits")
|
||||||
|
|
|
||||||
BIN
lnbits/static/images/lnbits-shop-dark.png
Normal file
BIN
lnbits/static/images/lnbits-shop-dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue