mirror of
https://github.com/lnbits/lnbits.git
synced 2025-10-09 20:12:34 +02:00
Merge remote-tracking branch 'origin/main' into fix-satoshi-formatting-nip5-index
This commit is contained in:
@@ -28,7 +28,9 @@ Going over the example extension's structure:
|
|||||||
Adding new dependencies
|
Adding new dependencies
|
||||||
-----------------------
|
-----------------------
|
||||||
|
|
||||||
If for some reason your extensions needs a new python package to work, you can add a new package using `venv`, or `poerty`:
|
DO NOT ADD NEW DEPENDENCIES. Try to use the dependencies that are availabe in `pyproject.toml`. Getting the LNbits project to accept a new dependency is time consuming and uncertain, and may result in your extension NOT being made available to others.
|
||||||
|
|
||||||
|
If for some reason your extensions must have a new python package to work, and its nees are not met in `pyproject.toml`, you can add a new package using `venv`, or `poerty`:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
$ poetry add <package>
|
$ poetry add <package>
|
||||||
@@ -37,8 +39,7 @@ $ ./venv/bin/pip install <package>
|
|||||||
```
|
```
|
||||||
|
|
||||||
**But we need an extra step to make sure LNbits doesn't break in production.**
|
**But we need an extra step to make sure LNbits doesn't break in production.**
|
||||||
Dependencies need to be added to `pyproject.toml` and `requirements.txt`, then tested by running on `venv` and `poetry`.
|
Dependencies need to be added to `pyproject.toml` and `requirements.txt`, then tested by running on `venv` and `poetry` compatability can be tested with `nix build .#checks.x86_64-linux.vmTest`.
|
||||||
`nix` compatability can be tested with `nix build .#checks.x86_64-linux.vmTest`.
|
|
||||||
|
|
||||||
|
|
||||||
SQLite to PostgreSQL migration
|
SQLite to PostgreSQL migration
|
||||||
|
BIN
lnbits/extensions/example/static/qrcode-example.png
Normal file
BIN
lnbits/extensions/example/static/qrcode-example.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.8 KiB |
BIN
lnbits/extensions/example/static/qrcode-example1.png
Normal file
BIN
lnbits/extensions/example/static/qrcode-example1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 34 KiB |
BIN
lnbits/extensions/example/static/websocket-example.png
Normal file
BIN
lnbits/extensions/example/static/websocket-example.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 49 KiB |
@@ -51,8 +51,15 @@
|
|||||||
<q-card flat>
|
<q-card flat>
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<div class="text-h5 q-mb-md">
|
<div class="text-h5 q-mb-md">
|
||||||
{{SITE_TITLE}} Extension Development Guide
|
Extension Development Guide
|
||||||
<small>(Collection of resources for extension developers)</small>
|
<small
|
||||||
|
>(also check the
|
||||||
|
<a
|
||||||
|
class="text-primary"
|
||||||
|
href="http://docs.lnbits.org/devs/development.html"
|
||||||
|
>docs</a
|
||||||
|
>)</small
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<q-card unelevated flat>
|
<q-card unelevated flat>
|
||||||
@@ -188,8 +195,8 @@
|
|||||||
<p>
|
<p>
|
||||||
LNbits uses
|
LNbits uses
|
||||||
<a href="https://vuejs.org/" class="text-primary">Vue</a>
|
<a href="https://vuejs.org/" class="text-primary">Vue</a>
|
||||||
components for best-in-class high-performance and responsive
|
for best-in-class, responsive and high-performance
|
||||||
performance.
|
components.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>Typical example of Vue components in a frontend script:</p>
|
<p>Typical example of Vue components in a frontend script:</p>
|
||||||
@@ -199,8 +206,7 @@
|
|||||||
/><br /><br />
|
/><br /><br />
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
In a page body, models can be called. <br />Content can be
|
Content can be conditionally rendered using Vue's
|
||||||
conditionally rendered using Vue's
|
|
||||||
<code class="bg-grey-3 text-black">v-if</code>:
|
<code class="bg-grey-3 text-black">v-if</code>:
|
||||||
</p>
|
</p>
|
||||||
<img
|
<img
|
||||||
@@ -220,6 +226,8 @@
|
|||||||
<q-tabs v-model="usefultab" align="left">
|
<q-tabs v-model="usefultab" align="left">
|
||||||
<q-tab name="magicalg">MAGICAL G</q-tab>
|
<q-tab name="magicalg">MAGICAL G</q-tab>
|
||||||
<q-tab name="exchange">EXCHANGE RATES</q-tab>
|
<q-tab name="exchange">EXCHANGE RATES</q-tab>
|
||||||
|
<q-tab name="qrcodes">QR CODES</q-tab>
|
||||||
|
<q-tab name="websockets">WEBSOCKETS</q-tab>
|
||||||
</q-tabs>
|
</q-tabs>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -255,6 +263,85 @@
|
|||||||
>:<br />
|
>:<br />
|
||||||
<img src="./static/conversion-example2.png" />
|
<img src="./static/conversion-example2.png" />
|
||||||
</q-tab-panel>
|
</q-tab-panel>
|
||||||
|
<q-tab-panel name="qrcodes" class="text-body1">
|
||||||
|
<div class="text-h5 q-mb-md">QR Codes</div>
|
||||||
|
<p>
|
||||||
|
For most purposes use Quasar's inbuilt VueQrcode library:
|
||||||
|
</p>
|
||||||
|
<img src="./static/qrcode-example1.png" />
|
||||||
|
<p>
|
||||||
|
LNbits does also include a handy
|
||||||
|
<a
|
||||||
|
href="../docs#/default/img_api_v1_qrcode__data__get"
|
||||||
|
class="text-primary"
|
||||||
|
>
|
||||||
|
QR code enpoint</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
{% raw %} You can use via
|
||||||
|
<a
|
||||||
|
href="/api/v1/qrcode/some-data-you-want-in-a-qrcode"
|
||||||
|
class="text-primary"
|
||||||
|
>{{protocol + location}}{% endraw
|
||||||
|
%}/api/v1/qrcode/some-data-you-want-in-a-qrcode:</a
|
||||||
|
><br />
|
||||||
|
<br />
|
||||||
|
<img src="./static/qrcode-example.png" />
|
||||||
|
<br />
|
||||||
|
<img
|
||||||
|
class="bg-white"
|
||||||
|
width="300px"
|
||||||
|
src="/api/v1/qrcode/some-data-you-want-in-a-qrcode"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<br />
|
||||||
|
</q-tab-panel>
|
||||||
|
|
||||||
|
<q-tab-panel name="websockets" class="text-body1">
|
||||||
|
<div class="text-h5 q-mb-md">Websockets</div>
|
||||||
|
<p>
|
||||||
|
Fastapi includes a great
|
||||||
|
<a
|
||||||
|
class="text-primary"
|
||||||
|
href="https://fastapi.tiangolo.com/advanced/websockets/#websockets-client"
|
||||||
|
>websocket tool</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
{% raw %}
|
||||||
|
<p>
|
||||||
|
A few LNbits extensions also make use of a weird and useful
|
||||||
|
websocket/GET tool built into LNbits, such as extensions
|
||||||
|
Copilot and LNURLDevices<br />
|
||||||
|
You can subscribe to websocket with
|
||||||
|
<code class="bg-grey-3 text-black"
|
||||||
|
>wss:{{location}}/api/v1/ws/{SOME-ID}</code
|
||||||
|
><br />
|
||||||
|
You can post to any clients subscribed to the endpoint with
|
||||||
|
<code class="bg-grey-3 text-black"
|
||||||
|
>{{protocol +
|
||||||
|
location}}/api/v1/ws/{SOME-ID}/{THE-DATA-YOU-WANT-TO-POST}</code
|
||||||
|
><br />
|
||||||
|
<br />
|
||||||
|
<strong
|
||||||
|
><div id="text-to-change">
|
||||||
|
DEMO: Hit
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
href="/api/v1/ws/32872r23g29/blah%20blah%20blah"
|
||||||
|
class="text-primary"
|
||||||
|
>{{protocol +
|
||||||
|
location}}/api/v1/ws/32872r23g29/blah%20blah%20blah</a
|
||||||
|
>
|
||||||
|
in a different browser window to change this text to
|
||||||
|
`blah blah blah`.
|
||||||
|
</div></strong
|
||||||
|
>
|
||||||
|
<br />
|
||||||
|
Function used in this demo:<br />
|
||||||
|
<img src="./static/websocket-example.png" /></p
|
||||||
|
></q-tab-panel>
|
||||||
|
|
||||||
|
{% endraw %}
|
||||||
</q-tab-panels>
|
</q-tab-panels>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
@@ -296,6 +383,8 @@
|
|||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
///// Declare models/variables /////
|
///// Declare models/variables /////
|
||||||
|
protocol: window.location.protocol,
|
||||||
|
location: '//' + window.location.hostname,
|
||||||
thingDialog: {
|
thingDialog: {
|
||||||
show: false,
|
show: false,
|
||||||
data: {}
|
data: {}
|
||||||
@@ -310,7 +399,7 @@
|
|||||||
},
|
},
|
||||||
///// Where functions live /////
|
///// Where functions live /////
|
||||||
methods: {
|
methods: {
|
||||||
exampleFunction(data) {
|
exampleFunction: function (data) {
|
||||||
var theData = data
|
var theData = data
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.request(
|
.request(
|
||||||
@@ -325,6 +414,28 @@
|
|||||||
LNbits.utils.notifyApiError(error) // Error will be passed to the frontend
|
LNbits.utils.notifyApiError(error) // Error will be passed to the frontend
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
initWs: async function () {
|
||||||
|
if (location.protocol !== 'http:') {
|
||||||
|
localUrl =
|
||||||
|
'wss://' +
|
||||||
|
document.domain +
|
||||||
|
':' +
|
||||||
|
location.port +
|
||||||
|
'/api/v1/ws/32872r23g29'
|
||||||
|
} else {
|
||||||
|
localUrl =
|
||||||
|
'ws://' +
|
||||||
|
document.domain +
|
||||||
|
':' +
|
||||||
|
location.port +
|
||||||
|
'/api/v1/ws/32872r23g29'
|
||||||
|
}
|
||||||
|
this.ws = new WebSocket(localUrl)
|
||||||
|
this.ws.addEventListener('message', async ({data}) => {
|
||||||
|
const res = data.toString()
|
||||||
|
document.getElementById('text-to-change').innerHTML = res
|
||||||
|
})
|
||||||
|
},
|
||||||
sendThingDialog() {
|
sendThingDialog() {
|
||||||
console.log(this.thingDialog)
|
console.log(this.thingDialog)
|
||||||
}
|
}
|
||||||
@@ -333,6 +444,7 @@
|
|||||||
created: function () {
|
created: function () {
|
||||||
self = this // Often used to run a real object, rather than the event (all a bit confusing really)
|
self = this // Often used to run a real object, rather than the event (all a bit confusing really)
|
||||||
self.exampleFunction('lorum')
|
self.exampleFunction('lorum')
|
||||||
|
self.initWs()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@@ -55,8 +55,16 @@
|
|||||||
></q-select>
|
></q-select>
|
||||||
<!-- </div> -->
|
<!-- </div> -->
|
||||||
<!-- </div> -->
|
<!-- </div> -->
|
||||||
|
<q-input
|
||||||
|
v-if="productDialog.url"
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="productDialog.data.image"
|
||||||
|
type="url"
|
||||||
|
label="Image URL"
|
||||||
|
></q-input>
|
||||||
<q-file
|
<q-file
|
||||||
|
v-else
|
||||||
class="q-pr-md"
|
class="q-pr-md"
|
||||||
filled
|
filled
|
||||||
dense
|
dense
|
||||||
@@ -79,6 +87,10 @@
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</q-file>
|
</q-file>
|
||||||
|
<q-toggle
|
||||||
|
:label="`${productDialog.url ? 'Insert image URL' : 'Upload image file'}`"
|
||||||
|
v-model="productDialog.url"
|
||||||
|
></q-toggle>
|
||||||
<q-input
|
<q-input
|
||||||
filled
|
filled
|
||||||
dense
|
dense
|
||||||
|
@@ -200,7 +200,10 @@
|
|||||||
:href="props.row.wallet"
|
:href="props.row.wallet"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
></q-btn>
|
></q-btn>
|
||||||
<q-tooltip> Link to pass to stall relay </q-tooltip>
|
<q-tooltip
|
||||||
|
>Disabled: link to pass to stall relays when using
|
||||||
|
nostr</q-tooltip
|
||||||
|
>
|
||||||
</q-td>
|
</q-td>
|
||||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
||||||
{{ col.value }}
|
{{ col.value }}
|
||||||
|
@@ -498,6 +498,7 @@
|
|||||||
},
|
},
|
||||||
productDialog: {
|
productDialog: {
|
||||||
show: false,
|
show: false,
|
||||||
|
url: true,
|
||||||
data: {}
|
data: {}
|
||||||
},
|
},
|
||||||
stallDialog: {
|
stallDialog: {
|
||||||
@@ -536,6 +537,9 @@
|
|||||||
methods: {
|
methods: {
|
||||||
resetDialog(dialog) {
|
resetDialog(dialog) {
|
||||||
this[dialog].show = false
|
this[dialog].show = false
|
||||||
|
if (dialog == 'productDialog') {
|
||||||
|
this[dialog].url = true
|
||||||
|
}
|
||||||
this[dialog].data = {}
|
this[dialog].data = {}
|
||||||
},
|
},
|
||||||
toggleDA(value, evt) {
|
toggleDA(value, evt) {
|
||||||
@@ -798,11 +802,17 @@
|
|||||||
var link = _.findWhere(self.products, {id: linkId})
|
var link = _.findWhere(self.products, {id: linkId})
|
||||||
|
|
||||||
self.productDialog.data = _.clone(link._data)
|
self.productDialog.data = _.clone(link._data)
|
||||||
|
if (self.productDialog.data.categories) {
|
||||||
self.productDialog.data.categories = self.productDialog.data.categories.split(
|
self.productDialog.data.categories = self.productDialog.data.categories.split(
|
||||||
','
|
','
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
if (self.productDialog.data.image.startsWith('data:')) {
|
||||||
|
self.productDialog.url = false
|
||||||
|
}
|
||||||
|
|
||||||
self.productDialog.show = true
|
self.productDialog.show = true
|
||||||
|
console.log(self.productDialog)
|
||||||
},
|
},
|
||||||
sendProductFormData: function () {
|
sendProductFormData: function () {
|
||||||
let _data = {...this.productDialog.data}
|
let _data = {...this.productDialog.data}
|
||||||
@@ -831,14 +841,8 @@
|
|||||||
let canvas = document.createElement('canvas')
|
let canvas = document.createElement('canvas')
|
||||||
canvas.setAttribute('width', fit.width)
|
canvas.setAttribute('width', fit.width)
|
||||||
canvas.setAttribute('height', fit.height)
|
canvas.setAttribute('height', fit.height)
|
||||||
await pica.resize(image, canvas, {
|
output = await pica.resize(image, canvas)
|
||||||
quality: 0,
|
this.productDialog.data.image = output.toDataURL('image/jpeg', 0.4)
|
||||||
alpha: true,
|
|
||||||
unsharpAmount: 95,
|
|
||||||
unsharpRadius: 0.9,
|
|
||||||
unsharpThreshold: 70
|
|
||||||
})
|
|
||||||
this.productDialog.data.image = canvas.toDataURL()
|
|
||||||
this.productDialog = {...this.productDialog}
|
this.productDialog = {...this.productDialog}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -113,6 +113,23 @@ async def api_market_product_create(
|
|||||||
if stall.currency != "sat":
|
if stall.currency != "sat":
|
||||||
data.price *= settings.fiat_base_multiplier
|
data.price *= settings.fiat_base_multiplier
|
||||||
|
|
||||||
|
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 product_id:
|
if product_id:
|
||||||
product = await get_market_product(product_id)
|
product = await get_market_product(product_id)
|
||||||
if not product:
|
if not product:
|
||||||
|
Reference in New Issue
Block a user