Merge branch 'main' into draggablecopilot
This commit is contained in:
@@ -66,7 +66,7 @@
|
||||
outline
|
||||
color="grey"
|
||||
type="a"
|
||||
href="https://github.com/lnbits/lnbits-legend"
|
||||
href="https://github.com/lnbits/lnbits"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>View project in GitHub</q-btn
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import json
|
||||
import os
|
||||
from typing import Callable, Dict, Union
|
||||
|
||||
import httpx
|
||||
|
||||
@@ -12,7 +13,9 @@ fiat_currencies = json.load(
|
||||
)
|
||||
)
|
||||
|
||||
exchange_rate_providers = {
|
||||
exchange_rate_providers: dict[
|
||||
str, dict[str, Union[str, Callable[[dict, dict], str]]]
|
||||
] = {
|
||||
"bitfinex": {
|
||||
"name": "Bitfinex",
|
||||
"domain": "bitfinex.com",
|
||||
@@ -65,17 +68,19 @@ async def fetch_fiat_exchange_rate(currency: str, provider: str):
|
||||
"to": currency.lower(),
|
||||
}
|
||||
|
||||
url = exchange_rate_providers[provider]["api_url"]
|
||||
if url:
|
||||
api_url_or_none = exchange_rate_providers[provider]["api_url"]
|
||||
if api_url_or_none is not None:
|
||||
api_url = str(api_url_or_none)
|
||||
for key in replacements.keys():
|
||||
url = url.replace("{" + key + "}", replacements[key])
|
||||
api_url = api_url.replace("{" + key + "}", replacements[key])
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.get(url)
|
||||
r = await client.get(api_url)
|
||||
r.raise_for_status()
|
||||
data = r.json()
|
||||
else:
|
||||
data = {}
|
||||
|
||||
getter = exchange_rate_providers[provider]["getter"]
|
||||
rate = float(getter(data, replacements))
|
||||
print(getter)
|
||||
if callable(getter):
|
||||
rate = float(getter(data, replacements))
|
||||
return rate
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import base64
|
||||
import hashlib
|
||||
import hmac
|
||||
import urllib
|
||||
from http import HTTPStatus
|
||||
from typing import Dict
|
||||
from urllib import parse
|
||||
|
||||
from starlette.requests import Request
|
||||
from fastapi import Request
|
||||
|
||||
|
||||
def generate_bleskomat_lnurl_hash(secret: str):
|
||||
@@ -22,7 +22,7 @@ def generate_bleskomat_lnurl_signature(
|
||||
elif api_key_encoding == "base64":
|
||||
key = base64.b64decode(api_key_secret)
|
||||
else:
|
||||
key = bytes(f"{api_key_secret}")
|
||||
key = bytes.fromhex(api_key_secret)
|
||||
return hmac.new(key=key, msg=payload.encode(), digestmod=hashlib.sha256).hexdigest()
|
||||
|
||||
|
||||
@@ -57,8 +57,8 @@ class LnurlValidationError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def prepare_lnurl_params(tag: str, query: Dict[str, str]):
|
||||
params = {}
|
||||
def prepare_lnurl_params(tag: str, query: dict) -> dict:
|
||||
params: dict = {}
|
||||
if not is_supported_lnurl_subprotocol(tag):
|
||||
raise LnurlValidationError(f'Unsupported subprotocol: "{tag}"')
|
||||
if tag == "withdrawRequest":
|
||||
@@ -85,15 +85,15 @@ def query_to_signing_payload(query: Dict[str, str]) -> str:
|
||||
payload = []
|
||||
for key in sorted_keys:
|
||||
if not key == "signature":
|
||||
encoded_key = urllib.parse.quote(key, safe=encode_uri_component_safe_chars)
|
||||
encoded_value = urllib.parse.quote(
|
||||
encoded_key = parse.quote(key, safe=encode_uri_component_safe_chars)
|
||||
encoded_value = parse.quote(
|
||||
query[key], safe=encode_uri_component_safe_chars
|
||||
)
|
||||
payload.append(f"{encoded_key}={encoded_value}")
|
||||
return "&".join(payload)
|
||||
|
||||
|
||||
unshorten_rules = {
|
||||
unshorten_rules: dict[str, dict] = {
|
||||
"query": {"n": "nonce", "s": "signature", "t": "tag"},
|
||||
"tags": {
|
||||
"c": "channelRequest",
|
||||
@@ -114,7 +114,7 @@ unshorten_rules = {
|
||||
}
|
||||
|
||||
|
||||
def unshorten_lnurl_query(query: Dict[str, str]) -> Dict[str, str]:
|
||||
def unshorten_lnurl_query(query: dict) -> Dict[str, str]:
|
||||
new_query = {}
|
||||
rules = unshorten_rules
|
||||
if "tag" in query:
|
||||
@@ -131,9 +131,9 @@ def unshorten_lnurl_query(query: Dict[str, str]) -> Dict[str, str]:
|
||||
if not tag in rules["params"]:
|
||||
raise LnurlValidationError(f'Unknown tag: "{tag}"')
|
||||
for key in query:
|
||||
if key in rules["params"][tag]:
|
||||
if key in rules["params"][str(tag)]:
|
||||
short_param_key = key
|
||||
long_param_key = rules["params"][tag][short_param_key]
|
||||
long_param_key = rules["params"][str(tag)][short_param_key]
|
||||
if short_param_key in query:
|
||||
new_query[long_param_key] = query[short_param_key]
|
||||
else:
|
||||
@@ -146,7 +146,7 @@ def unshorten_lnurl_query(query: Dict[str, str]) -> Dict[str, str]:
|
||||
if short_key in query:
|
||||
new_query[long_key] = query[short_key]
|
||||
else:
|
||||
new_query[long_key] = query[long_key]
|
||||
new_query[long_key] = query[str(long_key)]
|
||||
else:
|
||||
# Keep unknown key/value pairs unchanged:
|
||||
new_query[key] = query[key]
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import json
|
||||
import math
|
||||
import traceback
|
||||
from http import HTTPStatus
|
||||
|
||||
from loguru import logger
|
||||
@@ -28,7 +27,7 @@ from .helpers import (
|
||||
@bleskomat_ext.get("/u", name="bleskomat.api_bleskomat_lnurl")
|
||||
async def api_bleskomat_lnurl(req: Request):
|
||||
try:
|
||||
query = req.query_params
|
||||
query = dict(req.query_params)
|
||||
|
||||
# Unshorten query if "s" is used instead of "signature".
|
||||
if "s" in query:
|
||||
@@ -89,11 +88,15 @@ async def api_bleskomat_lnurl(req: Request):
|
||||
# Convert to msats:
|
||||
params[key] = int(amount_sats_less_fee * 1e3)
|
||||
except LnurlValidationError as e:
|
||||
raise LnurlHttpError(e.message, HTTPStatus.BAD_REQUEST)
|
||||
raise LnurlHttpError(str(e), HTTPStatus.BAD_REQUEST)
|
||||
# Create a new LNURL using the query parameters provided in the signed URL.
|
||||
params = json.JSONEncoder().encode(params)
|
||||
json_params = json.JSONEncoder().encode(params)
|
||||
lnurl = await create_bleskomat_lnurl(
|
||||
bleskomat=bleskomat, secret=secret, tag=tag, params=params, uses=1
|
||||
bleskomat=bleskomat,
|
||||
secret=secret,
|
||||
tag=tag,
|
||||
params=json_params,
|
||||
uses=1,
|
||||
)
|
||||
|
||||
# Reply with LNURL response object.
|
||||
|
||||
@@ -2,10 +2,9 @@ import json
|
||||
import time
|
||||
from typing import Dict
|
||||
|
||||
from fastapi.params import Query
|
||||
from fastapi import Query, Request
|
||||
from loguru import logger
|
||||
from pydantic import BaseModel, validator
|
||||
from starlette.requests import Request
|
||||
|
||||
from lnbits import bolt11
|
||||
from lnbits.core.services import PaymentFailure, pay_invoice
|
||||
@@ -80,7 +79,7 @@ class BleskomatLnurl(BaseModel):
|
||||
response["k1"] = secret
|
||||
return response
|
||||
|
||||
def validate_action(self, query: Dict[str, str]) -> None:
|
||||
def validate_action(self, query) -> None:
|
||||
tag = self.tag
|
||||
params = json.loads(self.params)
|
||||
# Perform tag-specific checks.
|
||||
@@ -109,7 +108,7 @@ class BleskomatLnurl(BaseModel):
|
||||
else:
|
||||
raise LnurlValidationError(f'Unknown subprotocol: "{tag}"')
|
||||
|
||||
async def execute_action(self, query: Dict[str, str]):
|
||||
async def execute_action(self, query):
|
||||
self.validate_action(query)
|
||||
used = False
|
||||
async with db.connect() as conn:
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from fastapi import Request
|
||||
from fastapi.params import Depends
|
||||
from fastapi import Depends, Request
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from starlette.responses import HTMLResponse
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ from starlette.exceptions import HTTPException
|
||||
|
||||
from lnbits.core.crud import get_user
|
||||
from lnbits.decorators import WalletTypeInfo, require_admin_key
|
||||
from lnbits.extensions.bleskomat.models import CreateBleskomat
|
||||
|
||||
from . import bleskomat_ext
|
||||
from .crud import (
|
||||
@@ -17,6 +16,7 @@ from .crud import (
|
||||
update_bleskomat,
|
||||
)
|
||||
from .exchange_rates import fetch_fiat_exchange_rate
|
||||
from .models import CreateBleskomat
|
||||
|
||||
|
||||
@bleskomat_ext.get("/api/v1/bleskomats")
|
||||
@@ -27,7 +27,8 @@ async def api_bleskomats(
|
||||
wallet_ids = [wallet.wallet.id]
|
||||
|
||||
if all_wallets:
|
||||
wallet_ids = (await get_user(wallet.wallet.user)).wallet_ids
|
||||
user = await get_user(wallet.wallet.user)
|
||||
wallet_ids = user.wallet_ids if user else []
|
||||
|
||||
return [bleskomat.dict() for bleskomat in await get_bleskomats(wallet_ids)]
|
||||
|
||||
@@ -54,9 +55,9 @@ async def api_bleskomat_create_or_update(
|
||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||
bleskomat_id=None,
|
||||
):
|
||||
fiat_currency = data.fiat_currency
|
||||
exchange_rate_provider = data.exchange_rate_provider
|
||||
try:
|
||||
fiat_currency = data.fiat_currency
|
||||
exchange_rate_provider = data.exchange_rate_provider
|
||||
await fetch_fiat_exchange_rate(
|
||||
currency=fiat_currency, provider=exchange_rate_provider
|
||||
)
|
||||
@@ -79,6 +80,7 @@ async def api_bleskomat_create_or_update(
|
||||
else:
|
||||
bleskomat = await create_bleskomat(wallet_id=wallet.wallet.id, data=data)
|
||||
|
||||
assert bleskomat
|
||||
return bleskomat.dict()
|
||||
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
<a
|
||||
class="text-secondary"
|
||||
target="_blank"
|
||||
href="https://github.com/lnbits/lnbits-legend/tree/main/lnbits/extensions/boltz"
|
||||
href="https://github.com/lnbits/lnbits/tree/main/lnbits/extensions/boltz"
|
||||
>More details</a
|
||||
>
|
||||
</p>
|
||||
|
||||
@@ -4,18 +4,19 @@
|
||||
<q-card class="q-pa-lg q-mb-xl">
|
||||
<q-card-section class="q-pa-none">
|
||||
<center>
|
||||
<q-icon
|
||||
name="account_balance"
|
||||
class="text-grey"
|
||||
style="font-size: 10rem"
|
||||
></q-icon>
|
||||
<h4 class="q-mt-none q-mb-md">{{ mint_name }}</h4>
|
||||
<q-img
|
||||
src="/cashu/static/image/cashu.png"
|
||||
spinner-color="white"
|
||||
style="max-width: 20%"
|
||||
></q-img>
|
||||
<h4 class="q-mt-sm q-mb-md">{{ mint_name }}</h4>
|
||||
<!-- <a class="text-secondary">Mint URL: {{testfield}} </a> <br /> -->
|
||||
<a
|
||||
class="text-secondary"
|
||||
class="q-my-xl text-white"
|
||||
style="font-size: 1.5rem"
|
||||
href="../wallet?mint_id={{ mint_id }}"
|
||||
>Open wallet</a
|
||||
>click to open wallet</a
|
||||
>
|
||||
</center>
|
||||
</q-card-section>
|
||||
@@ -58,24 +59,34 @@
|
||||
</p>
|
||||
<p>
|
||||
<strong>This service is in BETA</strong> <br />
|
||||
We hold no responsibility for people losing access to funds. Use at
|
||||
your own risk!
|
||||
Cashu is still experimental and in active development. There are
|
||||
likely bugs in this implementation so please use this with caution. We
|
||||
hold no responsibility for people losing access to funds. Use at your
|
||||
own risk!
|
||||
</p>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
{% endblock %} {% block scripts %}
|
||||
|
||||
<script>
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
||||
{% endblock %} {% block scripts %}
|
||||
|
||||
<script>
|
||||
Vue.component(VueQrcode.name, VueQrcode)
|
||||
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {
|
||||
testfield: 'asd',
|
||||
mintURL: {
|
||||
location: window.location,
|
||||
base_url: location.protocol + '//' + location.host + location.pathname
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -1479,15 +1479,15 @@ page_container %}
|
||||
},
|
||||
|
||||
constructOutputs: async function (amounts, secrets) {
|
||||
const blindedMessages = []
|
||||
const outputs = []
|
||||
const rs = []
|
||||
for (let i = 0; i < amounts.length; i++) {
|
||||
const {B_, r} = await step1Alice(secrets[i])
|
||||
blindedMessages.push({amount: amounts[i], B_: B_})
|
||||
outputs.push({amount: amounts[i], B_: B_})
|
||||
rs.push(r)
|
||||
}
|
||||
return {
|
||||
blindedMessages,
|
||||
outputs,
|
||||
rs
|
||||
}
|
||||
},
|
||||
@@ -1581,25 +1581,26 @@ page_container %}
|
||||
mintApi: async function (amounts, payment_hash, verbose = true) {
|
||||
/*
|
||||
asks the mint to check whether the invoice with payment_hash has been paid
|
||||
and requests signing of the attached outputs (blindedMessages)
|
||||
and requests signing of the attached outputs.
|
||||
*/
|
||||
console.log('### promises', payment_hash)
|
||||
try {
|
||||
let secrets = await this.generateSecrets(amounts)
|
||||
let {blindedMessages, rs} = await this.constructOutputs(
|
||||
amounts,
|
||||
secrets
|
||||
)
|
||||
let {outputs, rs} = await this.constructOutputs(amounts, secrets)
|
||||
const promises = await LNbits.api.request(
|
||||
'POST',
|
||||
`/cashu/api/v1/${this.mintId}/mint?payment_hash=${payment_hash}`,
|
||||
'',
|
||||
{
|
||||
blinded_messages: blindedMessages
|
||||
outputs
|
||||
}
|
||||
)
|
||||
console.log('### promises data', promises.data)
|
||||
let proofs = await this.constructProofs(promises.data, secrets, rs)
|
||||
console.log('### promises data', promises.data.promises)
|
||||
let proofs = await this.constructProofs(
|
||||
promises.data.promises,
|
||||
secrets,
|
||||
rs
|
||||
)
|
||||
return proofs
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
@@ -1682,16 +1683,11 @@ page_container %}
|
||||
'number of secrets does not match number of outputs.'
|
||||
)
|
||||
}
|
||||
let {blindedMessages, rs} = await this.constructOutputs(
|
||||
amounts,
|
||||
secrets
|
||||
)
|
||||
let {outputs, rs} = await this.constructOutputs(amounts, secrets)
|
||||
const payload = {
|
||||
amount,
|
||||
proofs,
|
||||
outputs: {
|
||||
blinded_messages: blindedMessages
|
||||
}
|
||||
outputs
|
||||
}
|
||||
|
||||
console.log('payload', JSON.stringify(payload))
|
||||
@@ -1881,10 +1877,7 @@ page_container %}
|
||||
'amount with fees',
|
||||
amount
|
||||
)
|
||||
// if (amount > balance()) {
|
||||
// LNbits.utils.notifyApiError('Balance too low')
|
||||
// return
|
||||
// }
|
||||
|
||||
let {fristProofs, scndProofs} = await this.splitToSend(
|
||||
this.proofs,
|
||||
amount
|
||||
@@ -2132,6 +2125,19 @@ page_container %}
|
||||
return paid
|
||||
},
|
||||
|
||||
findTokenForAmount: function (amount) {
|
||||
for (const token of this.proofs) {
|
||||
const index = token.promises?.findIndex(p => p.amount === amount)
|
||||
if (index >= 0) {
|
||||
return {
|
||||
promise: token.promises[index],
|
||||
secret: token.secrets[index],
|
||||
r: token.rs[index]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
////////////// WORKERS //////////////
|
||||
|
||||
clearAllWorkers: function () {
|
||||
@@ -2220,76 +2226,6 @@ page_container %}
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
findTokenForAmount: function (amount) {
|
||||
for (const token of this.proofs) {
|
||||
const index = token.promises?.findIndex(p => p.amount === amount)
|
||||
if (index >= 0) {
|
||||
return {
|
||||
promise: token.promises[index],
|
||||
secret: token.secrets[index],
|
||||
r: token.rs[index]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// checkInvoice: function () {
|
||||
// console.log('#### checkInvoice')
|
||||
// try {
|
||||
// const invoice = decode(this.payInvoiceData.data.request)
|
||||
|
||||
// const cleanInvoice = {
|
||||
// msat: invoice.human_readable_part.amount,
|
||||
// sat: invoice.human_readable_part.amount / 1000,
|
||||
// fsat: LNbits.utils.formatSat(
|
||||
// invoice.human_readable_part.amount / 1000
|
||||
// )
|
||||
// }
|
||||
|
||||
// _.each(invoice.data.tags, tag => {
|
||||
// if (_.isObject(tag) && _.has(tag, 'description')) {
|
||||
// if (tag.description === 'payment_hash') {
|
||||
// cleanInvoice.hash = tag.value
|
||||
// } else if (tag.description === 'description') {
|
||||
// cleanInvoice.description = tag.value
|
||||
// } else if (tag.description === 'expiry') {
|
||||
// var expireDate = new Date(
|
||||
// (invoice.data.time_stamp + tag.value) * 1000
|
||||
// )
|
||||
// cleanInvoice.expireDate = Quasar.utils.date.formatDate(
|
||||
// expireDate,
|
||||
// 'YYYY-MM-DDTHH:mm:ss.SSSZ'
|
||||
// )
|
||||
// cleanInvoice.expired = false // TODO
|
||||
// }
|
||||
// }
|
||||
|
||||
// this.payInvoiceData.invoice = cleanInvoice
|
||||
// })
|
||||
|
||||
// console.log(
|
||||
// '#### this.payInvoiceData.invoice',
|
||||
// this.payInvoiceData.invoice
|
||||
// )
|
||||
// } catch (error) {
|
||||
// this.$q.notify({
|
||||
// timeout: 5000,
|
||||
// type: 'warning',
|
||||
// message: 'Could not decode invoice',
|
||||
// caption: error + '',
|
||||
// position: 'top',
|
||||
// actions: [
|
||||
// {
|
||||
// icon: 'close',
|
||||
// color: 'white',
|
||||
// handler: () => {}
|
||||
// }
|
||||
// ]
|
||||
// })
|
||||
// throw error
|
||||
// }
|
||||
// },
|
||||
|
||||
////////////// STORAGE /////////////
|
||||
|
||||
getLocalstorageToFile: async function () {
|
||||
|
||||
@@ -12,7 +12,8 @@ from cashu.core.base import (
|
||||
GetMintResponse,
|
||||
Invoice,
|
||||
MeltRequest,
|
||||
MintRequest,
|
||||
PostMintRequest,
|
||||
PostMintResponse,
|
||||
PostSplitResponse,
|
||||
SplitRequest,
|
||||
)
|
||||
@@ -204,10 +205,10 @@ async def request_mint(cashu_id: str = Query(None), amount: int = 0) -> GetMintR
|
||||
|
||||
@cashu_ext.post("/api/v1/{cashu_id}/mint")
|
||||
async def mint(
|
||||
data: MintRequest,
|
||||
data: PostMintRequest,
|
||||
cashu_id: str = Query(None),
|
||||
payment_hash: str = Query(None),
|
||||
) -> List[BlindedSignature]:
|
||||
) -> PostMintResponse:
|
||||
"""
|
||||
Requests the minting of tokens belonging to a paid payment request.
|
||||
Call this endpoint after `GET /mint`.
|
||||
@@ -245,7 +246,7 @@ async def mint(
|
||||
)
|
||||
|
||||
try:
|
||||
total_requested = sum([bm.amount for bm in data.blinded_messages])
|
||||
total_requested = sum([bm.amount for bm in data.outputs])
|
||||
if total_requested > invoice.amount:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.PAYMENT_REQUIRED,
|
||||
@@ -257,10 +258,8 @@ async def mint(
|
||||
status_code=HTTPStatus.PAYMENT_REQUIRED, detail="Invoice not paid."
|
||||
)
|
||||
|
||||
promises = await ledger._generate_promises(
|
||||
B_s=data.blinded_messages, keyset=keyset
|
||||
)
|
||||
return promises
|
||||
promises = await ledger._generate_promises(B_s=data.outputs, keyset=keyset)
|
||||
return PostMintResponse(promises=promises)
|
||||
except (Exception, HTTPException) as e:
|
||||
logger.debug(f"Cashu: /melt {str(e) or getattr(e, 'detail')}")
|
||||
# unset issued flag because something went wrong
|
||||
@@ -274,10 +273,8 @@ async def mint(
|
||||
)
|
||||
else:
|
||||
# only used for testing when LIGHTNING=false
|
||||
promises = await ledger._generate_promises(
|
||||
B_s=data.blinded_messages, keyset=keyset
|
||||
)
|
||||
return promises
|
||||
promises = await ledger._generate_promises(B_s=data.outputs, keyset=keyset)
|
||||
return PostMintResponse(promises=promises)
|
||||
|
||||
|
||||
@cashu_ext.post("/api/v1/{cashu_id}/melt")
|
||||
@@ -421,7 +418,7 @@ async def split(
|
||||
)
|
||||
|
||||
amount = payload.amount
|
||||
outputs = payload.outputs.blinded_messages
|
||||
outputs = payload.outputs
|
||||
assert outputs, Exception("no outputs provided.")
|
||||
split_return = None
|
||||
try:
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
<a
|
||||
class="text-secondary"
|
||||
href="https://github.com/lnbits/lnbits-legend/tree/main/lnbits/extensions/lnaddress"
|
||||
href="https://github.com/lnbits/lnbits/tree/main/lnbits/extensions/lnaddress"
|
||||
>More details</a
|
||||
>
|
||||
<br />
|
||||
|
||||
@@ -195,7 +195,7 @@
|
||||
Check extension
|
||||
<a
|
||||
class="text-secondary"
|
||||
href="https://github.com/lnbits/lnbits-legend/blob/main/lnbits/extensions/lnaddress/README.md"
|
||||
href="https://github.com/lnbits/lnbits/blob/main/lnbits/extensions/lnaddress/README.md"
|
||||
>documentation!</a
|
||||
>
|
||||
</template>
|
||||
|
||||
@@ -3,7 +3,7 @@ from typing import List, Optional, Union
|
||||
from lnbits.helpers import urlsafe_short_hash
|
||||
|
||||
from . import db
|
||||
from .models import Address, CreateAddressData, CreateDomainData, Domain
|
||||
from .models import Address, CreateAddressData, CreateDomainData, Domain, EditDomainData
|
||||
|
||||
|
||||
async def get_domain(domain_id: str) -> Optional[Domain]:
|
||||
@@ -170,6 +170,26 @@ async def create_address_internal(domain_id: str, data: CreateAddressData) -> Ad
|
||||
return address
|
||||
|
||||
|
||||
async def update_domain_internal(wallet_id: str, data: EditDomainData) -> Domain:
|
||||
if data.currency != "Satoshis":
|
||||
amount = data.amount * 100
|
||||
else:
|
||||
amount = data.amount
|
||||
print(data)
|
||||
await db.execute(
|
||||
"""
|
||||
UPDATE nostrnip5.domains
|
||||
SET amount = ?, currency = ?
|
||||
WHERE id = ?
|
||||
""",
|
||||
(int(amount), data.currency, data.id),
|
||||
)
|
||||
|
||||
domain = await get_domain(data.id)
|
||||
assert domain, "Domain couldn't be updated"
|
||||
return domain
|
||||
|
||||
|
||||
async def create_domain_internal(wallet_id: str, data: CreateDomainData) -> Domain:
|
||||
domain_id = urlsafe_short_hash()
|
||||
|
||||
|
||||
@@ -24,6 +24,16 @@ class CreateDomainData(BaseModel):
|
||||
domain: str
|
||||
|
||||
|
||||
class EditDomainData(BaseModel):
|
||||
id: str
|
||||
currency: str
|
||||
amount: float = Query(..., ge=0.01)
|
||||
|
||||
@classmethod
|
||||
def from_row(cls, row: Row) -> "EditDomainData":
|
||||
return cls(**dict(row))
|
||||
|
||||
|
||||
class Domain(BaseModel):
|
||||
id: str
|
||||
wallet: str
|
||||
|
||||
@@ -73,6 +73,14 @@
|
||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||
@click="deleteDomain(props.row.id)"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
unelevated
|
||||
dense
|
||||
size="xs"
|
||||
icon="edit"
|
||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||
@click="editDomain(props.row.id)"
|
||||
></q-btn>
|
||||
</q-td>
|
||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.value }}
|
||||
@@ -226,6 +234,39 @@
|
||||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<q-dialog
|
||||
v-model="editFormDialog.show"
|
||||
position="top"
|
||||
@hide="closeFormDialog"
|
||||
>
|
||||
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
|
||||
<q-form @submit="saveEditedDomain" class="q-gutter-md">
|
||||
<q-select
|
||||
filled
|
||||
dense
|
||||
emit-value
|
||||
v-model="editFormDialog.data.currency"
|
||||
:options="currencyOptions"
|
||||
label="Currency *"
|
||||
></q-select>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="editFormDialog.data.amount"
|
||||
label="Amount"
|
||||
placeholder="How much do you want to charge?"
|
||||
></q-input>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn unelevated color="primary" type="submit">Update Amount</q-btn>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
||||
>Cancel</q-btn
|
||||
>
|
||||
</div>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<q-dialog
|
||||
v-model="addressFormDialog.show"
|
||||
position="top"
|
||||
@@ -513,6 +554,10 @@
|
||||
show: false,
|
||||
data: {}
|
||||
},
|
||||
editFormDialog: {
|
||||
show: false,
|
||||
data: {}
|
||||
},
|
||||
addressFormDialog: {
|
||||
show: false,
|
||||
data: {}
|
||||
@@ -578,6 +623,34 @@
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
saveEditedDomain: function () {
|
||||
var data = this.editFormDialog.data
|
||||
var self = this
|
||||
|
||||
LNbits.api
|
||||
.request(
|
||||
'PUT',
|
||||
'/nostrnip5/api/v1/domain',
|
||||
_.findWhere(this.g.user.wallets, {
|
||||
id: this.editFormDialog.data.wallet
|
||||
}).inkey,
|
||||
data
|
||||
)
|
||||
.then(function (response) {
|
||||
self.editFormDialog.show = false
|
||||
self.editFormDialog.data = {}
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
editDomain: function (domain_id) {
|
||||
var self = this
|
||||
var data = _.findWhere(this.domains, {id: domain_id})
|
||||
|
||||
self.editFormDialog.show = true
|
||||
self.editFormDialog.data = data
|
||||
},
|
||||
deleteDomain: function (domain_id) {
|
||||
var self = this
|
||||
var domain = _.findWhere(this.domains, {id: domain_id})
|
||||
|
||||
@@ -26,8 +26,14 @@ from .crud import (
|
||||
get_domain_by_name,
|
||||
get_domains,
|
||||
rotate_address,
|
||||
update_domain_internal,
|
||||
)
|
||||
from .models import (
|
||||
CreateAddressData,
|
||||
CreateDomainData,
|
||||
EditDomainData,
|
||||
RotateAddressData,
|
||||
)
|
||||
from .models import CreateAddressData, CreateDomainData, RotateAddressData
|
||||
|
||||
|
||||
@nostrnip5_ext.get("/api/v1/domains", status_code=HTTPStatus.OK)
|
||||
@@ -89,6 +95,16 @@ async def api_domain_create(
|
||||
return domain
|
||||
|
||||
|
||||
@nostrnip5_ext.put("/api/v1/domain", status_code=HTTPStatus.OK)
|
||||
async def api_domain_update(
|
||||
data: EditDomainData, wallet: WalletTypeInfo = Depends(get_key_type)
|
||||
):
|
||||
|
||||
domain = await update_domain_internal(wallet_id=wallet.wallet.id, data=data)
|
||||
|
||||
return domain
|
||||
|
||||
|
||||
@nostrnip5_ext.delete("/api/v1/domain/{domain_id}", status_code=HTTPStatus.CREATED)
|
||||
async def api_domain_delete(
|
||||
domain_id: str,
|
||||
|
||||
@@ -4,6 +4,15 @@ Vue.component(VueQrcode.name, VueQrcode)
|
||||
|
||||
const pica = window.pica()
|
||||
|
||||
function imgSizeFit(img, maxWidth = 1024, maxHeight = 768) {
|
||||
let ratio = Math.min(
|
||||
1,
|
||||
maxWidth / img.naturalWidth,
|
||||
maxHeight / img.naturalHeight
|
||||
)
|
||||
return {width: img.naturalWidth * ratio, height: img.naturalHeight * ratio}
|
||||
}
|
||||
|
||||
const defaultItemData = {
|
||||
unit: 'sat'
|
||||
}
|
||||
@@ -23,6 +32,7 @@ new Vue({
|
||||
},
|
||||
itemDialog: {
|
||||
show: false,
|
||||
urlImg: true,
|
||||
data: {...defaultItemData},
|
||||
units: ['sat']
|
||||
}
|
||||
@@ -41,6 +51,9 @@ new Vue({
|
||||
openUpdateDialog(itemId) {
|
||||
this.itemDialog.show = true
|
||||
let item = this.offlineshop.items.find(item => item.id === itemId)
|
||||
if (item.image.startsWith('data:')) {
|
||||
this.itemDialog.urlImg = false
|
||||
}
|
||||
this.itemDialog.data = item
|
||||
},
|
||||
imageAdded(file) {
|
||||
@@ -48,17 +61,12 @@ new Vue({
|
||||
let image = new Image()
|
||||
image.src = blobURL
|
||||
image.onload = async () => {
|
||||
let fit = imgSizeFit(image, 100, 100)
|
||||
let canvas = document.createElement('canvas')
|
||||
canvas.setAttribute('width', 100)
|
||||
canvas.setAttribute('height', 100)
|
||||
await pica.resize(image, canvas, {
|
||||
quality: 0,
|
||||
alpha: true,
|
||||
unsharpAmount: 95,
|
||||
unsharpRadius: 0.9,
|
||||
unsharpThreshold: 70
|
||||
})
|
||||
this.itemDialog.data.image = canvas.toDataURL()
|
||||
canvas.setAttribute('width', fit.width)
|
||||
canvas.setAttribute('height', fit.height)
|
||||
output = await pica.resize(image, canvas)
|
||||
this.itemDialog.data.image = output.toDataURL('image/jpeg', 0.4)
|
||||
this.itemDialog = {...this.itemDialog}
|
||||
}
|
||||
},
|
||||
@@ -155,6 +163,7 @@ new Vue({
|
||||
|
||||
this.loadShop()
|
||||
this.itemDialog.show = false
|
||||
this.itemDialog.urlImg = true
|
||||
this.itemDialog.data = {...defaultItemData}
|
||||
},
|
||||
toggleItem(itemId) {
|
||||
|
||||
@@ -237,7 +237,7 @@
|
||||
<q-responsive v-if="itemDialog.data.id" :ratio="1">
|
||||
<qrcode
|
||||
:value="'lightning:' + itemDialog.data.lnurl"
|
||||
:options="{width: 800}"
|
||||
:options="{width: 300}"
|
||||
class="rounded-borders"
|
||||
></qrcode>
|
||||
</q-responsive>
|
||||
@@ -266,7 +266,16 @@
|
||||
type="text"
|
||||
label="Brief description"
|
||||
></q-input>
|
||||
<q-input
|
||||
v-if="itemDialog.urlImg"
|
||||
filled
|
||||
dense
|
||||
v-model.trim="itemDialog.data.image"
|
||||
type="url"
|
||||
label="Image URL"
|
||||
></q-input>
|
||||
<q-file
|
||||
v-else
|
||||
filled
|
||||
dense
|
||||
capture="environment"
|
||||
@@ -288,6 +297,10 @@
|
||||
/>
|
||||
</template>
|
||||
</q-file>
|
||||
<q-toggle
|
||||
:label="`${itemDialog.urlImg ? 'Insert image URL' : 'Upload image file'}`"
|
||||
v-model="itemDialog.urlImg"
|
||||
></q-toggle>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
|
||||
@@ -60,6 +60,22 @@ async def api_add_or_update_item(
|
||||
):
|
||||
shop = await get_or_create_shop_by_wallet(wallet.wallet.id)
|
||||
assert shop
|
||||
if data.image:
|
||||
image_is_url = data.image.startswith("https://") or data.image.startswith(
|
||||
"http://"
|
||||
)
|
||||
|
||||
if not image_is_url:
|
||||
|
||||
def size(b64string):
|
||||
return int((len(b64string) * 3) / 4 - b64string.count("=", -2))
|
||||
|
||||
image_size = size(data.image) / 1024
|
||||
if image_size > 100:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail=f"Image size is too big, {int(image_size)}Kb. Max: 100kb, Compress the image at https://tinypng.com, or use an URL.",
|
||||
)
|
||||
if data.unit != "sat":
|
||||
data.price = data.price * 100
|
||||
if item_id == None:
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
from http import HTTPStatus
|
||||
from typing import List, Optional, Union
|
||||
|
||||
from lnbits.helpers import urlsafe_short_hash
|
||||
|
||||
from . import db
|
||||
from .models import CreateEmail, CreateEmailaddress, Emailaddresses, Emails
|
||||
from .models import CreateEmail, CreateEmailaddress, Email, Emailaddress
|
||||
from .smtp import send_mail
|
||||
|
||||
|
||||
@@ -17,7 +16,7 @@ def get_test_mail(email, testemail):
|
||||
)
|
||||
|
||||
|
||||
async def create_emailaddress(data: CreateEmailaddress) -> Emailaddresses:
|
||||
async def create_emailaddress(data: CreateEmailaddress) -> Emailaddress:
|
||||
|
||||
emailaddress_id = urlsafe_short_hash()
|
||||
|
||||
@@ -50,7 +49,7 @@ async def create_emailaddress(data: CreateEmailaddress) -> Emailaddresses:
|
||||
return new_emailaddress
|
||||
|
||||
|
||||
async def update_emailaddress(emailaddress_id: str, **kwargs) -> Emailaddresses:
|
||||
async def update_emailaddress(emailaddress_id: str, **kwargs) -> Emailaddress:
|
||||
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
|
||||
await db.execute(
|
||||
f"UPDATE smtp.emailaddress SET {q} WHERE id = ?",
|
||||
@@ -65,30 +64,22 @@ async def update_emailaddress(emailaddress_id: str, **kwargs) -> Emailaddresses:
|
||||
await send_mail(row, email)
|
||||
|
||||
assert row, "Newly updated emailaddress couldn't be retrieved"
|
||||
return Emailaddresses(**row)
|
||||
return Emailaddress(**row)
|
||||
|
||||
|
||||
async def get_emailaddress(emailaddress_id: str) -> Optional[Emailaddresses]:
|
||||
async def get_emailaddress(emailaddress_id: str) -> Optional[Emailaddress]:
|
||||
row = await db.fetchone(
|
||||
"SELECT * FROM smtp.emailaddress WHERE id = ?", (emailaddress_id,)
|
||||
)
|
||||
return Emailaddresses(**row) if row else None
|
||||
return Emailaddress(**row) if row else None
|
||||
|
||||
|
||||
async def get_emailaddress_by_email(email: str) -> Optional[Emailaddresses]:
|
||||
async def get_emailaddress_by_email(email: str) -> Optional[Emailaddress]:
|
||||
row = await db.fetchone("SELECT * FROM smtp.emailaddress WHERE email = ?", (email,))
|
||||
return Emailaddresses(**row) if row else None
|
||||
return Emailaddress(**row) if row else None
|
||||
|
||||
|
||||
# async def get_emailAddressByEmail(email: str) -> Optional[Emails]:
|
||||
# row = await db.fetchone(
|
||||
# "SELECT s.*, d.emailaddress as emailaddress FROM smtp.email s INNER JOIN smtp.emailaddress d ON (s.emailaddress_id = d.id) WHERE s.emailaddress = ?",
|
||||
# (email,),
|
||||
# )
|
||||
# return Subdomains(**row) if row else None
|
||||
|
||||
|
||||
async def get_emailaddresses(wallet_ids: Union[str, List[str]]) -> List[Emailaddresses]:
|
||||
async def get_emailaddresses(wallet_ids: Union[str, List[str]]) -> List[Emailaddress]:
|
||||
if isinstance(wallet_ids, str):
|
||||
wallet_ids = [wallet_ids]
|
||||
|
||||
@@ -97,21 +88,22 @@ async def get_emailaddresses(wallet_ids: Union[str, List[str]]) -> List[Emailadd
|
||||
f"SELECT * FROM smtp.emailaddress WHERE wallet IN ({q})", (*wallet_ids,)
|
||||
)
|
||||
|
||||
return [Emailaddresses(**row) for row in rows]
|
||||
return [Emailaddress(**row) for row in rows]
|
||||
|
||||
|
||||
async def delete_emailaddress(emailaddress_id: str) -> None:
|
||||
await db.execute("DELETE FROM smtp.emailaddress WHERE id = ?", (emailaddress_id,))
|
||||
|
||||
|
||||
## create emails
|
||||
async def create_email(payment_hash, wallet, data: CreateEmail) -> Emails:
|
||||
async def create_email(wallet: str, data: CreateEmail, payment_hash: str = "") -> Email:
|
||||
id = urlsafe_short_hash()
|
||||
await db.execute(
|
||||
"""
|
||||
INSERT INTO smtp.email (id, wallet, emailaddress_id, subject, receiver, message, paid)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO smtp.email (id, payment_hash, wallet, emailaddress_id, subject, receiver, message, paid)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
id,
|
||||
payment_hash,
|
||||
wallet,
|
||||
data.emailaddress_id,
|
||||
@@ -122,36 +114,34 @@ async def create_email(payment_hash, wallet, data: CreateEmail) -> Emails:
|
||||
),
|
||||
)
|
||||
|
||||
new_email = await get_email(payment_hash)
|
||||
new_email = await get_email(id)
|
||||
assert new_email, "Newly created email couldn't be retrieved"
|
||||
return new_email
|
||||
|
||||
|
||||
async def set_email_paid(payment_hash: str) -> Emails:
|
||||
email = await get_email(payment_hash)
|
||||
async def set_email_paid(payment_hash: str) -> bool:
|
||||
email = await get_email_by_payment_hash(payment_hash)
|
||||
if email and email.paid == False:
|
||||
await db.execute(
|
||||
"""
|
||||
UPDATE smtp.email
|
||||
SET paid = true
|
||||
WHERE id = ?
|
||||
""",
|
||||
(payment_hash,),
|
||||
f"UPDATE smtp.email SET paid = true WHERE payment_hash = ?", (payment_hash,)
|
||||
)
|
||||
new_email = await get_email(payment_hash)
|
||||
assert new_email, "Newly paid email couldn't be retrieved"
|
||||
return new_email
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
async def get_email(email_id: str) -> Optional[Emails]:
|
||||
async def get_email_by_payment_hash(payment_hash: str) -> Optional[Email]:
|
||||
row = await db.fetchone(
|
||||
"SELECT s.*, d.email as emailaddress FROM smtp.email s INNER JOIN smtp.emailaddress d ON (s.emailaddress_id = d.id) WHERE s.id = ?",
|
||||
(email_id,),
|
||||
f"SELECT * FROM smtp.email WHERE payment_hash = ?", (payment_hash,)
|
||||
)
|
||||
return Emails(**row) if row else None
|
||||
return Email(**row) if row else None
|
||||
|
||||
|
||||
async def get_emails(wallet_ids: Union[str, List[str]]) -> List[Emails]:
|
||||
async def get_email(id: str) -> Optional[Email]:
|
||||
row = await db.fetchone(f"SELECT * FROM smtp.email WHERE id = ?", (id,))
|
||||
return Email(**row) if row else None
|
||||
|
||||
|
||||
async def get_emails(wallet_ids: Union[str, List[str]]) -> List[Email]:
|
||||
if isinstance(wallet_ids, str):
|
||||
wallet_ids = [wallet_ids]
|
||||
|
||||
@@ -161,7 +151,7 @@ async def get_emails(wallet_ids: Union[str, List[str]]) -> List[Emails]:
|
||||
(*wallet_ids,),
|
||||
)
|
||||
|
||||
return [Emails(**row) for row in rows]
|
||||
return [Email(**row) for row in rows]
|
||||
|
||||
|
||||
async def delete_email(email_id: str) -> None:
|
||||
|
||||
@@ -33,3 +33,7 @@ async def m001_initial(db):
|
||||
);
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
async def m002_add_payment_hash(db):
|
||||
await db.execute(f"ALTER TABLE smtp.email ADD COLUMN payment_hash TEXT;")
|
||||
|
||||
@@ -15,7 +15,7 @@ class CreateEmailaddress(BaseModel):
|
||||
cost: int = Query(..., ge=0)
|
||||
|
||||
|
||||
class Emailaddresses(BaseModel):
|
||||
class Emailaddress(BaseModel):
|
||||
id: str
|
||||
wallet: str
|
||||
email: str
|
||||
@@ -36,7 +36,7 @@ class CreateEmail(BaseModel):
|
||||
message: str = Query(...)
|
||||
|
||||
|
||||
class Emails(BaseModel):
|
||||
class Email(BaseModel):
|
||||
id: str
|
||||
wallet: str
|
||||
emailaddress_id: str
|
||||
|
||||
@@ -4,83 +4,107 @@ import time
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
from email.utils import formatdate
|
||||
from http import HTTPStatus
|
||||
from smtplib import SMTP_SSL as SMTP
|
||||
from typing import Union
|
||||
|
||||
from loguru import logger
|
||||
from starlette.exceptions import HTTPException
|
||||
|
||||
from .models import CreateEmail, CreateEmailaddress, Email, Emailaddress
|
||||
|
||||
|
||||
async def send_mail(
|
||||
emailaddress: Union[Emailaddress, CreateEmailaddress],
|
||||
email: Union[Email, CreateEmail],
|
||||
):
|
||||
smtp_client = SmtpService(emailaddress)
|
||||
message = smtp_client.create_message(email)
|
||||
await smtp_client.send_mail(email.receiver, message)
|
||||
|
||||
|
||||
def valid_email(s):
|
||||
# https://regexr.com/2rhq7
|
||||
pat = "[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?"
|
||||
pat = r"[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?"
|
||||
if re.match(pat, s):
|
||||
return True
|
||||
msg = f"SMTP - invalid email: {s}."
|
||||
logger.error(msg)
|
||||
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=msg)
|
||||
log = f"SMTP - invalid email: {s}."
|
||||
logger.error(log)
|
||||
raise Exception(log)
|
||||
|
||||
|
||||
async def send_mail(emailaddress, email):
|
||||
valid_email(emailaddress.email)
|
||||
valid_email(email.receiver)
|
||||
class SmtpService:
|
||||
def __init__(self, emailaddress: Union[Emailaddress, CreateEmailaddress]) -> None:
|
||||
self.sender = emailaddress.email
|
||||
self.smtp_server = emailaddress.smtp_server
|
||||
self.smtp_port = emailaddress.smtp_port
|
||||
self.smtp_user = emailaddress.smtp_user
|
||||
self.smtp_password = emailaddress.smtp_password
|
||||
|
||||
ts = time.time()
|
||||
date = formatdate(ts, True)
|
||||
|
||||
msg = MIMEMultipart("alternative")
|
||||
msg = MIMEMultipart("alternative")
|
||||
msg["Date"] = date
|
||||
msg["Subject"] = email.subject
|
||||
msg["From"] = emailaddress.email
|
||||
msg["To"] = email.receiver
|
||||
|
||||
signature = "Email sent anonymiously by LNbits Sendmail extension."
|
||||
text = f"""
|
||||
{email.message}
|
||||
|
||||
{signature}
|
||||
"""
|
||||
|
||||
html = f"""
|
||||
<html>
|
||||
<head></head>
|
||||
<body>
|
||||
<p>{email.message}<p>
|
||||
<br>
|
||||
<p>{signature}</p>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
part1 = MIMEText(text, "plain")
|
||||
part2 = MIMEText(html, "html")
|
||||
msg.attach(part1)
|
||||
msg.attach(part2)
|
||||
|
||||
try:
|
||||
conn = SMTP(
|
||||
host=emailaddress.smtp_server, port=emailaddress.smtp_port, timeout=10
|
||||
def render_email(self, email: Union[Email, CreateEmail]):
|
||||
signature: str = "Email sent by LNbits SMTP extension."
|
||||
text = f"{email.message}\n\n{signature}"
|
||||
html = (
|
||||
"""
|
||||
<html>
|
||||
<head></head>
|
||||
<body>
|
||||
<p>"""
|
||||
+ email.message
|
||||
+ """</p>
|
||||
<p>"""
|
||||
+ signature
|
||||
+ """</p>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
)
|
||||
logger.debug("SMTP - connected to smtp server.")
|
||||
# conn.set_debuglevel(True)
|
||||
except:
|
||||
msg = f"SMTP - error connecting to smtp server: {emailaddress.smtp_server}:{emailaddress.smtp_port}."
|
||||
logger.error(msg)
|
||||
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=msg)
|
||||
try:
|
||||
conn.login(emailaddress.smtp_user, emailaddress.smtp_password)
|
||||
logger.debug("SMTP - successful login to smtp server.")
|
||||
except:
|
||||
msg = f"SMTP - error login into smtp {emailaddress.smtp_user}."
|
||||
logger.error(msg)
|
||||
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=msg)
|
||||
try:
|
||||
conn.sendmail(emailaddress.email, email.receiver, msg.as_string())
|
||||
logger.debug("SMTP - successfully send email.")
|
||||
except socket.error as e:
|
||||
msg = f"SMTP - error sending email: {str(e)}."
|
||||
logger.error(msg)
|
||||
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=msg)
|
||||
finally:
|
||||
conn.quit()
|
||||
return text, html
|
||||
|
||||
def create_message(self, email: Union[Email, CreateEmail]):
|
||||
ts = time.time()
|
||||
date = formatdate(ts, True)
|
||||
|
||||
msg = MIMEMultipart("alternative")
|
||||
msg["Date"] = date
|
||||
msg["Subject"] = email.subject
|
||||
msg["From"] = self.sender
|
||||
msg["To"] = email.receiver
|
||||
|
||||
text, html = self.render_email(email)
|
||||
|
||||
part1 = MIMEText(text, "plain")
|
||||
part2 = MIMEText(html, "html")
|
||||
msg.attach(part1)
|
||||
msg.attach(part2)
|
||||
return msg
|
||||
|
||||
async def send_mail(self, receiver, msg: MIMEMultipart):
|
||||
|
||||
valid_email(self.sender)
|
||||
valid_email(receiver)
|
||||
|
||||
try:
|
||||
conn = SMTP(host=self.smtp_server, port=int(self.smtp_port), timeout=10)
|
||||
logger.debug("SMTP - connected to smtp server.")
|
||||
# conn.set_debuglevel(True)
|
||||
except:
|
||||
log = f"SMTP - error connecting to smtp server: {self.smtp_server}:{self.smtp_port}."
|
||||
logger.debug(log)
|
||||
raise Exception(log)
|
||||
|
||||
try:
|
||||
conn.login(self.smtp_user, self.smtp_password)
|
||||
logger.debug("SMTP - successful login to smtp server.")
|
||||
except:
|
||||
log = f"SMTP - error login into smtp {self.smtp_user}."
|
||||
logger.error(log)
|
||||
raise Exception(log)
|
||||
|
||||
try:
|
||||
conn.sendmail(self.sender, receiver, msg.as_string())
|
||||
logger.debug("SMTP - successfully send email.")
|
||||
except socket.error as e:
|
||||
log = f"SMTP - error sending email: {str(e)}."
|
||||
logger.error(log)
|
||||
raise Exception(log)
|
||||
finally:
|
||||
conn.quit()
|
||||
|
||||
@@ -5,7 +5,7 @@ from loguru import logger
|
||||
from lnbits.core.models import Payment
|
||||
from lnbits.tasks import register_invoice_listener
|
||||
|
||||
from .crud import get_email, get_emailaddress, set_email_paid
|
||||
from .crud import get_email_by_payment_hash, get_emailaddress, set_email_paid
|
||||
from .smtp import send_mail
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ async def on_invoice_paid(payment: Payment) -> None:
|
||||
if payment.extra.get("tag") != "smtp":
|
||||
return
|
||||
|
||||
email = await get_email(payment.checking_id)
|
||||
email = await get_email_by_payment_hash(payment.checking_id)
|
||||
if not email:
|
||||
logger.error("SMTP: email can not by fetched")
|
||||
return
|
||||
|
||||
@@ -57,6 +57,14 @@
|
||||
:href="props.row.displayUrl"
|
||||
target="_blank"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
unelevated
|
||||
dense
|
||||
size="xs"
|
||||
icon="email"
|
||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||
@click="showEmailDialog(props.row.id)"
|
||||
></q-btn>
|
||||
</q-td>
|
||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.value }}
|
||||
@@ -154,6 +162,42 @@
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
<q-dialog v-model="emailDialog.show" position="top">
|
||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||
<q-form @submit="sendEmail()" class="q-gutter-md">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="emailDialog.data.receiver"
|
||||
type="text"
|
||||
label="Receiver"
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="emailDialog.data.subject"
|
||||
type="text"
|
||||
label="Subject"
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="emailDialog.data.message"
|
||||
type="textarea"
|
||||
label="Message "
|
||||
></q-input>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
unelevated
|
||||
color="primary"
|
||||
:disable="emailDialog.data.receiver == '' || emailDialog.data.subject == '' || emailDialog.data.message == ''"
|
||||
type="submit"
|
||||
>Submit</q-btn
|
||||
>
|
||||
</div>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
<q-dialog v-model="emailaddressDialog.show" position="top">
|
||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||
<q-form @submit="sendFormData" class="q-gutter-md">
|
||||
@@ -316,10 +360,10 @@
|
||||
emailsTable: {
|
||||
columns: [
|
||||
{
|
||||
name: 'emailaddress',
|
||||
name: 'emailaddress_id',
|
||||
align: 'left',
|
||||
label: 'From',
|
||||
field: 'emailaddress'
|
||||
field: 'emailaddress_id'
|
||||
},
|
||||
{
|
||||
name: 'receiver',
|
||||
@@ -350,6 +394,10 @@
|
||||
rowsPerPage: 10
|
||||
}
|
||||
},
|
||||
emailDialog: {
|
||||
show: false,
|
||||
data: {}
|
||||
},
|
||||
emailaddressDialog: {
|
||||
show: false,
|
||||
data: {}
|
||||
@@ -453,6 +501,33 @@
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
sendEmail: function () {
|
||||
var self = this
|
||||
var emailaddress = _.findWhere(this.emailaddresses, {
|
||||
id: self.emailDialog.data.emailaddress_id
|
||||
})
|
||||
var wallet = _.findWhere(this.g.user.wallets, {
|
||||
id: emailaddress.wallet
|
||||
})
|
||||
LNbits.api
|
||||
.request(
|
||||
'POST',
|
||||
'/smtp/api/v1/email/' + emailaddress.id + '/send',
|
||||
wallet.adminkey,
|
||||
self.emailDialog.data
|
||||
)
|
||||
.then(function (response) {
|
||||
self.emailDialog.show = false
|
||||
self.emailDialog.data = {}
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
showEmailDialog: function (emailaddress_id) {
|
||||
this.emailDialog.data.emailaddress_id = emailaddress_id
|
||||
this.emailDialog.show = true
|
||||
},
|
||||
updateEmailaddressDialog: function (formId) {
|
||||
var link = _.findWhere(this.emailaddresses, {id: formId})
|
||||
this.emailaddressDialog.data = _.clone(link)
|
||||
|
||||
@@ -4,7 +4,7 @@ from fastapi import Depends, HTTPException, Query
|
||||
|
||||
from lnbits.core.crud import get_user
|
||||
from lnbits.core.services import check_transaction_status, create_invoice
|
||||
from lnbits.decorators import WalletTypeInfo, get_key_type
|
||||
from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
|
||||
|
||||
from . import smtp_ext
|
||||
from .crud import (
|
||||
@@ -13,13 +13,14 @@ from .crud import (
|
||||
delete_email,
|
||||
delete_emailaddress,
|
||||
get_email,
|
||||
get_email_by_payment_hash,
|
||||
get_emailaddress,
|
||||
get_emailaddresses,
|
||||
get_emails,
|
||||
update_emailaddress,
|
||||
)
|
||||
from .models import CreateEmail, CreateEmailaddress
|
||||
from .smtp import valid_email
|
||||
from .smtp import send_mail, valid_email
|
||||
|
||||
|
||||
## EMAILS
|
||||
@@ -37,13 +38,14 @@ async def api_email(
|
||||
|
||||
@smtp_ext.get("/api/v1/email/{payment_hash}")
|
||||
async def api_smtp_send_email(payment_hash):
|
||||
email = await get_email(payment_hash)
|
||||
email = await get_email_by_payment_hash(payment_hash)
|
||||
if not email:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST, detail="paymenthash is wrong"
|
||||
)
|
||||
|
||||
emailaddress = await get_emailaddress(email.emailaddress_id)
|
||||
assert emailaddress
|
||||
|
||||
try:
|
||||
status = await check_transaction_status(email.wallet, payment_hash)
|
||||
@@ -59,11 +61,9 @@ async def api_smtp_send_email(payment_hash):
|
||||
|
||||
@smtp_ext.post("/api/v1/email/{emailaddress_id}")
|
||||
async def api_smtp_make_email(emailaddress_id, data: CreateEmail):
|
||||
|
||||
valid_email(data.receiver)
|
||||
|
||||
emailaddress = await get_emailaddress(emailaddress_id)
|
||||
# If the request is coming for the non-existant emailaddress
|
||||
if not emailaddress:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
@@ -94,6 +94,26 @@ async def api_smtp_make_email(emailaddress_id, data: CreateEmail):
|
||||
return {"payment_hash": payment_hash, "payment_request": payment_request}
|
||||
|
||||
|
||||
@smtp_ext.post(
|
||||
"/api/v1/email/{emailaddress_id}/send", dependencies=[Depends(require_admin_key)]
|
||||
)
|
||||
async def api_smtp_make_email_send(emailaddress_id, data: CreateEmail):
|
||||
valid_email(data.receiver)
|
||||
emailaddress = await get_emailaddress(emailaddress_id)
|
||||
if not emailaddress:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail="Emailaddress address does not exist.",
|
||||
)
|
||||
email = await create_email(wallet=emailaddress.wallet, data=data)
|
||||
if not email:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="Email could not be fetched."
|
||||
)
|
||||
await send_mail(emailaddress, email)
|
||||
return {"sent": True}
|
||||
|
||||
|
||||
@smtp_ext.delete("/api/v1/email/{email_id}")
|
||||
async def api_email_delete(email_id, g: WalletTypeInfo = Depends(get_key_type)):
|
||||
email = await get_email(email_id)
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
<a
|
||||
class="text-secondary"
|
||||
href="https://github.com/lnbits/lnbits-legend/tree/main/lnbits/extensions/subdomains"
|
||||
href="https://github.com/lnbits/lnbits/tree/main/lnbits/extensions/subdomains"
|
||||
>More details</a
|
||||
>
|
||||
<br />
|
||||
|
||||
@@ -8,7 +8,7 @@ from typing import AsyncGenerator, Dict, Optional
|
||||
import httpx
|
||||
from loguru import logger
|
||||
|
||||
# TODO: https://github.com/lnbits/lnbits-legend/issues/764
|
||||
# TODO: https://github.com/lnbits/lnbits/issues/764
|
||||
# mypy https://github.com/aaugustin/websockets/issues/940
|
||||
from websockets import connect # type: ignore
|
||||
from websockets.exceptions import (
|
||||
|
||||
Reference in New Issue
Block a user