This commit is contained in:
benarc 2022-01-27 12:24:38 +00:00 committed by Tiago vasconcelos
parent f4580955b9
commit b1bfef8784
13 changed files with 2569 additions and 0 deletions

View file

@ -0,0 +1,9 @@
<h1>Diagon Alley</h1>
<h2>A movable market stand</h2>
Make a list of products to sell, point the list to an relay (or many), stack sats.
Diagon Alley is a movable market stand, for anon transactions. You then give permission for an relay to list those products. Delivery addresses are sent through the Lightning Network.
<img src="https://i.imgur.com/P1tvBSG.png">
<h2>API endpoints</h2>
<code>curl -X GET http://YOUR-TOR-ADDRESS</code>

View file

@ -0,0 +1,16 @@
from quart import Blueprint
from lnbits.db import Database
db = Database("ext_diagonalley")
diagonalley_ext: Blueprint = Blueprint(
"diagonalley", __name__, static_folder="static", template_folder="templates"
)
from .views_api import * # noqa
from .views import * # noqa
from .tasks import register_listeners
from lnbits.tasks import record_async
diagonalley_ext.record(record_async(register_listeners))

View file

@ -0,0 +1,6 @@
{
"name": "Diagon Alley",
"short_description": "Movable anonymous market stand",
"icon": "add_shopping_cart",
"contributors": ["benarc","DeanH"]
}

View file

@ -0,0 +1,395 @@
from base64 import urlsafe_b64encode
from uuid import uuid4
from typing import List, Optional, Union
from lnbits.settings import WALLET
# from lnbits.db import open_ext_db
from lnbits.db import SQLITE
from . import db
from .models import Products, Orders, Stalls, Zones
import httpx
from lnbits.helpers import urlsafe_short_hash
import re
regex = re.compile(
r"^(?:http|ftp)s?://" # http:// or https://
r"(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|"
r"localhost|"
r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})"
r"(?::\d+)?"
r"(?:/?|[/?]\S+)$",
re.IGNORECASE,
)
###Products
async def create_diagonalley_product(
*,
stall_id: str,
product: str,
categories: str,
description: str,
image: Optional[str] = None,
price: int,
quantity: int,
shippingzones: str,
) -> Products:
returning = "" if db.type == SQLITE else "RETURNING ID"
method = db.execute if db.type == SQLITE else db.fetchone
product_id = urlsafe_short_hash()
# with open_ext_db("diagonalley") as db:
result = await (method)(
f"""
INSERT INTO diagonalley.products (id, stall, product, categories, description, image, price, quantity, shippingzones)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
{returning}
""",
(
product_id,
stall_id,
product,
categories,
description,
image,
price,
quantity,
),
)
product = await get_diagonalley_product(product_id)
assert product, "Newly created product couldn't be retrieved"
return product
async def update_diagonalley_product(product_id: str, **kwargs) -> Optional[Stalls]:
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
# with open_ext_db("diagonalley") as db:
await db.execute(
f"UPDATE diagonalley.products SET {q} WHERE id = ?",
(*kwargs.values(), product_id),
)
row = await db.fetchone(
"SELECT * FROM diagonalley.products WHERE id = ?", (product_id,)
)
return get_diagonalley_stall(product_id)
async def get_diagonalley_product(product_id: str) -> Optional[Products]:
row = await db.fetchone(
"SELECT * FROM diagonalley.products WHERE id = ?", (product_id,)
)
return Products.from_row(row) if row else None
async def get_diagonalley_products(wallet_ids: Union[str, List[str]]) -> List[Products]:
if isinstance(wallet_ids, str):
wallet_ids = [wallet_ids]
# with open_ext_db("diagonalley") as db:
q = ",".join(["?"] * len(wallet_ids))
rows = await db.fetchall(
f"""
SELECT * FROM diagonalley.products WHERE stall IN ({q})
""",
(*wallet_ids,),
)
return [Products.from_row(row) for row in rows]
async def delete_diagonalley_product(product_id: str) -> None:
await db.execute("DELETE FROM diagonalley.products WHERE id = ?", (product_id,))
###zones
async def create_diagonalley_zone(
*,
wallet: Optional[str] = None,
cost: Optional[int] = 0,
countries: Optional[str] = None,
) -> Zones:
returning = "" if db.type == SQLITE else "RETURNING ID"
method = db.execute if db.type == SQLITE else db.fetchone
zone_id = urlsafe_short_hash()
result = await (method)(
f"""
INSERT INTO diagonalley.zones (
id,
wallet,
cost,
countries
)
VALUES (?, ?, ?, ?)
{returning}
""",
(zone_id, wallet, cost, countries),
)
zone = await get_diagonalley_zone(zone_id)
assert zone, "Newly created zone couldn't be retrieved"
return zone
async def update_diagonalley_zone(zone_id: str, **kwargs) -> Optional[Zones]:
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
await db.execute(
f"UPDATE diagonalley.zones SET {q} WHERE id = ?",
(*kwargs.values(), zone_id),
)
row = await db.fetchone("SELECT * FROM diagonalley.zones WHERE id = ?", (zone_id,))
return Zones.from_row(row) if row else None
async def get_diagonalley_zone(zone_id: str) -> Optional[Zones]:
row = await db.fetchone("SELECT * FROM diagonalley.zones WHERE id = ?", (zone_id,))
return Zones.from_row(row) if row else None
async def get_diagonalley_zones(wallet_ids: Union[str, List[str]]) -> List[Zones]:
if isinstance(wallet_ids, str):
wallet_ids = [wallet_ids]
print(wallet_ids)
q = ",".join(["?"] * len(wallet_ids))
rows = await db.fetchall(
f"SELECT * FROM diagonalley.zones WHERE wallet IN ({q})", (*wallet_ids,)
)
for r in rows:
try:
x = httpx.get(r["zoneaddress"] + "/" + r["ratingkey"])
if x.status_code == 200:
await db.execute(
"UPDATE diagonalley.zones SET online = ? WHERE id = ?",
(
True,
r["id"],
),
)
else:
await db.execute(
"UPDATE diagonalley.zones SET online = ? WHERE id = ?",
(
False,
r["id"],
),
)
except:
print("An exception occurred")
q = ",".join(["?"] * len(wallet_ids))
rows = await db.fetchall(
f"SELECT * FROM diagonalley.zones WHERE wallet IN ({q})", (*wallet_ids,)
)
return [Zones.from_row(row) for row in rows]
async def delete_diagonalley_zone(zone_id: str) -> None:
await db.execute("DELETE FROM diagonalley.zones WHERE id = ?", (zone_id,))
###Stalls
async def create_diagonalley_stall(
*,
wallet: str,
name: str,
publickey: str,
privatekey: str,
relays: str,
shippingzones: str,
) -> Stalls:
returning = "" if db.type == SQLITE else "RETURNING ID"
method = db.execute if db.type == SQLITE else db.fetchone
stall_id = urlsafe_short_hash()
result = await (method)(
f"""
INSERT INTO diagonalley.stalls (
id,
wallet,
name,
publickey,
privatekey,
relays,
shippingzones
)
VALUES (?, ?, ?, ?, ?, ?, ?)
{returning}
""",
(stall_id, wallet, name, publickey, privatekey, relays, shippingzones),
)
stall = await get_diagonalley_stall(stall_id)
assert stall, "Newly created stall couldn't be retrieved"
return stall
async def update_diagonalley_stall(stall_id: str, **kwargs) -> Optional[Stalls]:
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
await db.execute(
f"UPDATE diagonalley.stalls SET {q} WHERE id = ?",
(*kwargs.values(), stall_id),
)
row = await db.fetchone(
"SELECT * FROM diagonalley.stalls WHERE id = ?", (stall_id,)
)
return Stalls.from_row(row) if row else None
async def get_diagonalley_stall(stall_id: str) -> Optional[Stalls]:
roww = await db.fetchone(
"SELECT * FROM diagonalley.stalls WHERE id = ?", (stall_id,)
)
try:
x = httpx.get(roww["stalladdress"] + "/" + roww["ratingkey"])
if x.status_code == 200:
await db.execute(
"UPDATE diagonalley.stalls SET online = ? WHERE id = ?",
(
True,
stall_id,
),
)
else:
await db.execute(
"UPDATE diagonalley.stalls SET online = ? WHERE id = ?",
(
False,
stall_id,
),
)
except:
print("An exception occurred")
# with open_ext_db("diagonalley") as db:
row = await db.fetchone(
"SELECT * FROM diagonalley.stalls WHERE id = ?", (stall_id,)
)
return Stalls.from_row(row) if row else None
async def get_diagonalley_stalls(wallet_ids: Union[str, List[str]]) -> List[Stalls]:
if isinstance(wallet_ids, str):
wallet_ids = [wallet_ids]
q = ",".join(["?"] * len(wallet_ids))
rows = await db.fetchall(
f"SELECT * FROM diagonalley.stalls WHERE wallet IN ({q})", (*wallet_ids,)
)
for r in rows:
try:
x = httpx.get(r["stalladdress"] + "/" + r["ratingkey"])
if x.status_code == 200:
await db.execute(
"UPDATE diagonalley.stalls SET online = ? WHERE id = ?",
(
True,
r["id"],
),
)
else:
await db.execute(
"UPDATE diagonalley.stalls SET online = ? WHERE id = ?",
(
False,
r["id"],
),
)
except:
print("An exception occurred")
q = ",".join(["?"] * len(wallet_ids))
rows = await db.fetchall(
f"SELECT * FROM diagonalley.stalls WHERE wallet IN ({q})", (*wallet_ids,)
)
return [Stalls.from_row(row) for row in rows]
async def delete_diagonalley_stall(stall_id: str) -> None:
await db.execute("DELETE FROM diagonalley.stalls WHERE id = ?", (stall_id,))
###Orders
async def create_diagonalley_order(
*,
productid: str,
wallet: str,
product: str,
quantity: int,
shippingzone: str,
address: str,
email: str,
invoiceid: str,
paid: bool,
shipped: bool,
) -> Orders:
returning = "" if db.type == SQLITE else "RETURNING ID"
method = db.execute if db.type == SQLITE else db.fetchone
order_id = urlsafe_short_hash()
result = await (method)(
f"""
INSERT INTO diagonalley.orders (id, productid, wallet, product,
quantity, shippingzone, address, email, invoiceid, paid, shipped)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
{returning}
""",
(
order_id,
productid,
wallet,
product,
quantity,
shippingzone,
address,
email,
invoiceid,
False,
False,
),
)
if db.type == SQLITE:
order_id = result._result_proxy.lastrowid
else:
order_id = result[0]
link = await get_diagonalley_order(order_id)
assert link, "Newly created link couldn't be retrieved"
return link
async def get_diagonalley_order(order_id: str) -> Optional[Orders]:
row = await db.fetchone(
"SELECT * FROM diagonalley.orders WHERE id = ?", (order_id,)
)
return Orders.from_row(row) if row else None
async def get_diagonalley_orders(wallet_ids: Union[str, List[str]]) -> List[Orders]:
if isinstance(wallet_ids, str):
wallet_ids = [wallet_ids]
q = ",".join(["?"] * len(wallet_ids))
rows = await db.fetchall(
f"SELECT * FROM diagonalley.orders WHERE wallet IN ({q})", (*wallet_ids,)
)
#
return [Orders.from_row(row) for row in rows]
async def delete_diagonalley_order(order_id: str) -> None:
await db.execute("DELETE FROM diagonalley.orders WHERE id = ?", (order_id,))

View file

@ -0,0 +1,69 @@
async def m001_initial(db):
"""
Initial products table.
"""
await db.execute(
"""
CREATE TABLE diagonalley.products (
id TEXT PRIMARY KEY,
stall TEXT NOT NULL,
product TEXT NOT NULL,
categories TEXT NOT NULL,
description TEXT NOT NULL,
image TEXT NOT NULL,
price INTEGER NOT NULL,
quantity INTEGER NOT NULL
);
"""
)
"""
Initial stalls table.
"""
await db.execute(
"""
CREATE TABLE diagonalley.stalls (
id TEXT PRIMARY KEY,
wallet TEXT NOT NULL,
name TEXT NOT NULL,
publickey TEXT NOT NULL,
privatekey TEXT NOT NULL,
relays TEXT NOT NULL
);
"""
)
"""
Initial zones table.
"""
await db.execute(
"""
CREATE TABLE diagonalley.zones (
id TEXT PRIMARY KEY,
wallet TEXT NOT NULL,
cost TEXT NOT NULL,
countries TEXT NOT NULL
);
"""
)
"""
Initial orders table.
"""
await db.execute(
"""
CREATE TABLE diagonalley.orders (
id TEXT PRIMARY KEY,
productid TEXT NOT NULL,
wallet TEXT NOT NULL,
product TEXT NOT NULL,
quantity INTEGER NOT NULL,
shippingzone INTEGER NOT NULL,
address TEXT NOT NULL,
email TEXT NOT NULL,
invoiceid TEXT NOT NULL,
paid BOOLEAN NOT NULL,
shipped BOOLEAN NOT NULL
);
"""
)

View file

@ -0,0 +1,57 @@
from urllib.parse import urlparse, urlunparse, parse_qs, urlencode, ParseResult
from starlette.requests import Request
from fastapi.param_functions import Query
from typing import Optional, Dict
from lnbits.lnurl import encode as lnurl_encode # type: ignore
from lnurl.types import LnurlPayMetadata # type: ignore
from pydantic import BaseModel
import json
from sqlite3 import Row
class Stalls(BaseModel):
id: str = Query(None)
wallet: str = Query(None)
name: str = Query(None)
publickey: str = Query(None)
privatekey: str = Query(None)
relays: str = Query(None)
class createStalls(BaseModel):
wallet: str = Query(None)
name: str = Query(None)
publickey: str = Query(None)
privatekey: str = Query(None)
relays: str = Query(None)
shippingzones: str = Query(None)
class Products(BaseModel):
id: str = Query(None)
stall: str = Query(None)
product: str = Query(None)
categories: str = Query(None)
description: str = Query(None)
image: str = Query(None)
price: int = Query(0)
quantity: int = Query(0)
class Zones(BaseModel):
id: str = Query(None)
wallet: str = Query(None)
cost: str = Query(None)
countries: str = Query(None)
class Orders(BaseModel):
id: str = Query(None)
productid: str = Query(None)
stall: str = Query(None)
product: str = Query(None)
quantity: int = Query(0)
shippingzone: int = Query(0)
address: str = Query(None)
email: str = Query(None)
invoiceid: str = Query(None)
paid: bool
shipped: bool

View file

@ -0,0 +1,824 @@
/* globals Quasar, Vue, _, VueQrcode, windowMixin, LNbits, LOCALE */
Vue.component(VueQrcode.name, VueQrcode)
const pica = window.pica()
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {
products: [],
orders: [],
stalls: [],
zones: [],
shippedModel: false,
shippingZoneOptions: [
'Australia',
'Austria',
'Belgium',
'Brazil',
'Canada',
'Denmark',
'Finland',
'France*',
'Germany',
'Greece',
'Hong Kong',
'Hungary',
'Ireland',
'Indonesia',
'Israel',
'Italy',
'Japan',
'Kazakhstan',
'Korea',
'Luxembourg',
'Malaysia',
'Mexico',
'Netherlands',
'New Zealand',
'Norway',
'Poland',
'Portugal',
'Russia',
'Saudi Arabia',
'Singapore',
'Spain',
'Sweden',
'Switzerland',
'Thailand',
'Turkey',
'Ukraine',
'United Kingdom**',
'United States***',
'Vietnam',
'China'
],
categories: [
'Fashion (clothing and accessories)',
'Health (and beauty)',
'Toys (and baby equipment)',
'Media (Books and CDs)',
'Groceries (Food and Drink)',
'Technology (Phones and Computers)',
'Home (furniture and accessories)',
'Gifts (flowers, cards, etc)'
],
relayOptions: [
'wss://nostr-relay.herokuapp.com/ws',
'wss://nostr-relay.bigsun.xyz/ws',
'wss://freedom-relay.herokuapp.com/ws'
],
label: '',
ordersTable: {
columns: [
{
name: 'product',
align: 'left',
label: 'Product',
field: 'product'
},
{
name: 'quantity',
align: 'left',
label: 'Quantity',
field: 'quantity'
},
{
name: 'address',
align: 'left',
label: 'Address',
field: 'address'
},
{
name: 'invoiceid',
align: 'left',
label: 'InvoiceID',
field: 'invoiceid'
},
{name: 'paid', align: 'left', label: 'Paid', field: 'paid'},
{name: 'shipped', align: 'left', label: 'Shipped', field: 'shipped'}
],
pagination: {
rowsPerPage: 10
}
},
productsTable: {
columns: [
{
name: 'stall',
align: 'left',
label: 'Stall',
field: 'stall'
},
{
name: 'product',
align: 'left',
label: 'Product',
field: 'product'
},
{
name: 'description',
align: 'left',
label: 'Description',
field: 'description'
},
{
name: 'categories',
align: 'left',
label: 'Categories',
field: 'categories'
},
{name: 'price', align: 'left', label: 'Price', field: 'price'},
{
name: 'quantity',
align: 'left',
label: 'Quantity',
field: 'quantity'
},
{name: 'id', align: 'left', label: 'ID', field: 'id'}
],
pagination: {
rowsPerPage: 10
}
},
stallTable: {
columns: [
{
name: 'id',
align: 'left',
label: 'ID',
field: 'id'
},
{
name: 'name',
align: 'left',
label: 'Name',
field: 'name'
},
{
name: 'wallet',
align: 'left',
label: 'Wallet',
field: 'wallet'
},
{
name: 'publickey',
align: 'left',
label: 'Public key',
field: 'publickey'
},
{
name: 'privatekey',
align: 'left',
label: 'Private key',
field: 'privatekey'
}
],
pagination: {
rowsPerPage: 10
}
},
zonesTable: {
columns: [
{
name: 'id',
align: 'left',
label: 'ID',
field: 'id'
},
{
name: 'countries',
align: 'left',
label: 'Countries',
field: 'countries'
},
{
name: 'cost',
align: 'left',
label: 'Cost',
field: 'cost'
}
],
pagination: {
rowsPerPage: 10
}
},
productDialog: {
show: false,
data: {}
},
stallDialog: {
show: false,
data: {}
},
zoneDialog: {
show: false,
data: {}
},
shopDialog: {
show: false,
data: {activate: false}
},
orderDialog: {
show: false,
data: {}
},
relayDialog: {
show: false,
data: {}
}
}
},
computed: {
categoryOther: function () {
cats = trim(this.productDialog.data.categories.split(','))
for (let i = 0; i < cats.length; i++) {
if (cats[i] == 'Others') {
return true
}
}
return false
}
},
methods: {
////////////////////////////////////////
////////////////STALLS//////////////////
////////////////////////////////////////
getStalls: function () {
var self = this
LNbits.api
.request(
'GET',
'/diagonalley/api/v1/stalls?all_wallets',
this.g.user.wallets[0].inkey
)
.then(function (response) {
self.stalls = response.data.map(function (obj) {
console.log(obj)
return mapDiagonAlley(obj)
})
})
},
openStallUpdateDialog: function (linkId) {
var self = this
var link = _.findWhere(self.stalls, {id: linkId})
this.stallDialog.data = _.clone(link._data)
this.stallDialog.show = true
},
sendStallFormData: function () {
if (this.stallDialog.data.id) {
} else {
var data = {
name: this.stallDialog.data.name,
wallet: this.stallDialog.data.wallet,
publickey: this.stallDialog.data.publickey,
privatekey: this.stallDialog.data.privatekey,
relays: this.stallDialog.data.relays
}
}
if (this.stallDialog.data.id) {
this.updateStall(this.stallDialog.data)
} else {
this.createStall(data)
}
},
updateStall: function (data) {
var self = this
LNbits.api
.request(
'PUT',
'/diagonalley/api/v1/stalls' + data.id,
_.findWhere(self.g.user.wallets, {
id: self.stallDialog.data.wallet
}).inkey,
_.pick(data, 'name', 'wallet', 'publickey', 'privatekey')
)
.then(function (response) {
self.stalls = _.reject(self.stalls, function (obj) {
return obj.id == data.id
})
self.stalls.push(mapDiagonAlley(response.data))
self.stallDialog.show = false
self.stallDialog.data = {}
data = {}
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
createStall: function (data) {
var self = this
LNbits.api
.request(
'POST',
'/diagonalley/api/v1/stalls',
_.findWhere(self.g.user.wallets, {
id: self.stallDialog.data.wallet
}).inkey,
data
)
.then(function (response) {
self.stalls.push(mapDiagonAlley(response.data))
self.stallDialog.show = false
self.stallDialog.data = {}
data = {}
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
deleteStall: function (stallId) {
var self = this
var stall = _.findWhere(self.stalls, {id: stallId})
LNbits.utils
.confirmDialog('Are you sure you want to delete this Stall link?')
.onOk(function () {
LNbits.api
.request(
'DELETE',
'/diagonalley/api/v1/stalls/' + stallId,
_.findWhere(self.g.user.wallets, {id: stall.wallet}).inkey
)
.then(function (response) {
self.stalls = _.reject(self.stalls, function (obj) {
return obj.id == stallId
})
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
})
},
exportStallsCSV: function () {
LNbits.utils.exportCSV(this.stallsTable.columns, this.stalls)
},
////////////////////////////////////////
///////////////PRODUCTS/////////////////
////////////////////////////////////////
getProducts: function () {
var self = this
LNbits.api
.request(
'GET',
'/diagonalley/api/v1/products?all_stalls',
this.g.user.wallets[0].inkey
)
.then(function (response) {
self.products = response.data.map(function (obj) {
return mapDiagonAlley(obj)
})
})
},
openProductUpdateDialog: function (linkId) {
var self = this
var link = _.findWhere(self.products, {id: linkId})
self.productDialog.data = _.clone(link._data)
self.productDialog.show = true
},
sendProductFormData: function () {
if (this.productDialog.data.id) {
} else {
var data = {
product: this.productDialog.data.product,
categories:
this.productDialog.data.categories +
this.productDialog.categoriesextra,
description: this.productDialog.data.description,
image: this.productDialog.data.image,
price: this.productDialog.data.price,
quantity: this.productDialog.data.quantity
}
}
if (this.productDialog.data.id) {
this.updateProduct(this.productDialog.data)
} else {
this.createProduct(data)
}
},
imageAdded(file) {
let blobURL = URL.createObjectURL(file)
let image = new Image()
image.src = blobURL
image.onload = async () => {
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.productDialog.data.image = canvas.toDataURL()
this.productDialog = {...this.productDialog}
}
},
imageCleared() {
this.productDialog.data.image = null
this.productDialog = {...this.productDialog}
},
updateProduct: function (data) {
var self = this
LNbits.api
.request(
'PUT',
'/diagonalley/api/v1/products' + data.id,
_.findWhere(self.g.user.wallets, {
id: self.productDialog.data.wallet
}).inkey,
_.pick(
data,
'shopname',
'relayaddress',
'shippingzone1',
'zone1cost',
'shippingzone2',
'zone2cost',
'email'
)
)
.then(function (response) {
self.products = _.reject(self.products, function (obj) {
return obj.id == data.id
})
self.products.push(mapDiagonAlley(response.data))
self.productDialog.show = false
self.productDialog.data = {}
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
createProduct: function (data) {
var self = this
LNbits.api
.request(
'POST',
'/diagonalley/api/v1/products',
_.findWhere(self.g.user.wallets, {
id: self.productDialog.data.wallet
}).inkey,
data
)
.then(function (response) {
self.products.push(mapDiagonAlley(response.data))
self.productDialog.show = false
self.productDialog.data = {}
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
deleteProduct: function (productId) {
var self = this
var product = _.findWhere(this.products, {id: productId})
LNbits.utils
.confirmDialog('Are you sure you want to delete this products link?')
.onOk(function () {
LNbits.api
.request(
'DELETE',
'/diagonalley/api/v1/products/' + productId,
_.findWhere(self.g.user.wallets, {id: product.wallet}).inkey
)
.then(function (response) {
self.products = _.reject(self.products, function (obj) {
return obj.id == productId
})
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
})
},
exportProductsCSV: function () {
LNbits.utils.exportCSV(this.productsTable.columns, this.products)
},
////////////////////////////////////////
//////////////////ZONE//////////////////
////////////////////////////////////////
getZones: function () {
var self = this
LNbits.api
.request(
'GET',
'/diagonalley/api/v1/zones?all_wallets',
this.g.user.wallets[0].inkey
)
.then(function (response) {
self.zones = response.data.map(function (obj) {
return mapDiagonAlley(obj)
})
})
},
openZoneUpdateDialog: function (linkId) {
var self = this
var link = _.findWhere(self.zones, {id: linkId})
this.zoneDialog.data = _.clone(link._data)
this.zoneDialog.show = true
},
sendZoneFormData: function () {
if (this.zoneDialog.data.id) {
} else {
var data = {
countries: toString(this.zoneDialog.data.countries),
cost: parseInt(this.zoneDialog.data.cost)
}
}
if (this.zoneDialog.data.id) {
this.updateZone(this.zoneDialog.data)
} else {
this.createZone(data)
}
},
updateZone: function (data) {
var self = this
LNbits.api
.request(
'PUT',
'/diagonalley/api/v1/zones' + data.id,
_.findWhere(self.g.user.wallets, {
id: self.zoneDialog.data.wallet
}).inkey,
_.pick(data, 'countries', 'cost')
)
.then(function (response) {
self.zones = _.reject(self.zones, function (obj) {
return obj.id == data.id
})
self.zones.push(mapDiagonAlley(response.data))
self.zoneDialog.show = false
self.zoneDialog.data = {}
data = {}
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
createZone: function (data) {
var self = this
console.log(self.g.user.wallets[0])
console.log(data)
LNbits.api
.request(
'POST',
'/diagonalley/api/v1/zones',
self.g.user.wallets[0].inkey,
data
)
.then(function (response) {
self.zones.push(mapDiagonAlley(response.data))
self.zoneDialog.show = false
self.zoneDialog.data = {}
data = {}
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
deleteZone: function (zoneId) {
var self = this
var zone = _.findWhere(self.zones, {id: zoneId})
LNbits.utils
.confirmDialog('Are you sure you want to delete this Zone link?')
.onOk(function () {
LNbits.api
.request(
'DELETE',
'/diagonalley/api/v1/zones/' + zoneId,
_.findWhere(self.g.user.wallets, {id: zone.wallet}).inkey
)
.then(function (response) {
self.zones = _.reject(self.zones, function (obj) {
return obj.id == zoneId
})
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
})
},
exportZonesCSV: function () {
LNbits.utils.exportCSV(this.zonesTable.columns, this.zones)
},
////////////////////////////////////////
//////////////////SHOP//////////////////
////////////////////////////////////////
getShops: function () {
var self = this
LNbits.api
.request(
'GET',
'/diagonalley/api/v1/shops?all_wallets',
this.g.user.wallets[0].inkey
)
.then(function (response) {
self.shops = response.data.map(function (obj) {
return mapDiagonAlley(obj)
})
})
},
openShopUpdateDialog: function (linkId) {
var self = this
var link = _.findWhere(self.shops, {id: linkId})
this.shopDialog.data = _.clone(link._data)
this.shopDialog.show = true
},
sendShopFormData: function () {
if (this.shopDialog.data.id) {
} else {
var data = {
countries: this.shopDialog.data.countries,
cost: this.shopDialog.data.cost
}
}
if (this.shopDialog.data.id) {
this.updateZone(this.shopDialog.data)
} else {
this.createZone(data)
}
},
updateShop: function (data) {
var self = this
LNbits.api
.request(
'PUT',
'/diagonalley/api/v1/shops' + data.id,
_.findWhere(self.g.user.wallets, {
id: self.shopDialog.data.wallet
}).inkey,
_.pick(data, 'countries', 'cost')
)
.then(function (response) {
self.shops = _.reject(self.shops, function (obj) {
return obj.id == data.id
})
self.shops.push(mapDiagonAlley(response.data))
self.shopDialog.show = false
self.shopDialog.data = {}
data = {}
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
createShop: function (data) {
var self = this
console.log('cuntywoo')
LNbits.api
.request(
'POST',
'/diagonalley/api/v1/shops',
_.findWhere(self.g.user.wallets, {
id: self.shopDialog.data.wallet
}).inkey,
data
)
.then(function (response) {
self.shops.push(mapDiagonAlley(response.data))
self.shopDialog.show = false
self.shopDialog.data = {}
data = {}
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
deleteShop: function (shopId) {
var self = this
var shop = _.findWhere(self.shops, {id: shopId})
LNbits.utils
.confirmDialog('Are you sure you want to delete this Shop link?')
.onOk(function () {
LNbits.api
.request(
'DELETE',
'/diagonalley/api/v1/shops/' + shopId,
_.findWhere(self.g.user.wallets, {id: shop.wallet}).inkey
)
.then(function (response) {
self.shops = _.reject(self.shops, function (obj) {
return obj.id == shopId
})
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
})
},
exportShopsCSV: function () {
LNbits.utils.exportCSV(this.shopsTable.columns, this.shops)
},
////////////////////////////////////////
////////////////ORDERS//////////////////
////////////////////////////////////////
getOrders: function () {
var self = this
LNbits.api
.request(
'GET',
'/diagonalley/api/v1/orders?all_wallets',
this.g.user.wallets[0].inkey
)
.then(function (response) {
self.orders = response.data.map(function (obj) {
return mapDiagonAlley(obj)
})
})
},
createOrder: function () {
var data = {
address: this.orderDialog.data.address,
email: this.orderDialog.data.email,
quantity: this.orderDialog.data.quantity,
shippingzone: this.orderDialog.data.shippingzone
}
var self = this
LNbits.api
.request(
'POST',
'/diagonalley/api/v1/orders',
_.findWhere(self.g.user.wallets, {id: self.orderDialog.data.wallet})
.inkey,
data
)
.then(function (response) {
self.orders.push(mapDiagonAlley(response.data))
self.orderDialog.show = false
self.orderDialog.data = {}
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
deleteOrder: function (orderId) {
var self = this
var order = _.findWhere(self.orders, {id: orderId})
LNbits.utils
.confirmDialog('Are you sure you want to delete this order link?')
.onOk(function () {
LNbits.api
.request(
'DELETE',
'/diagonalley/api/v1/orders/' + orderId,
_.findWhere(self.g.user.wallets, {id: order.wallet}).inkey
)
.then(function (response) {
self.orders = _.reject(self.orders, function (obj) {
return obj.id == orderId
})
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
})
},
shipOrder: function (order_id) {
var self = this
LNbits.api
.request(
'GET',
'/diagonalley/api/v1/orders/shipped/' + order_id,
this.g.user.wallets[0].inkey
)
.then(function (response) {
self.orders = response.data.map(function (obj) {
return mapDiagonAlley(obj)
})
})
},
exportOrdersCSV: function () {
LNbits.utils.exportCSV(this.ordersTable.columns, this.orders)
}
},
created: function () {
if (this.g.user.wallets.length) {
this.getStalls()
this.getProducts()
this.getZones()
this.getOrders()
}
}
})

View file

@ -0,0 +1,29 @@
import asyncio
from lnbits.core.models import Payment
from lnbits.tasks import register_invoice_listener
from .crud import get_ticket, set_ticket_paid
async def wait_for_paid_invoices():
invoice_queue = asyncio.Queue()
register_invoice_listener(invoice_queue)
while True:
payment = await invoice_queue.get()
await on_invoice_paid(payment)
async def on_invoice_paid(payment: Payment) -> None:
if "lnticket" != payment.extra.get("tag"):
# not a lnticket invoice
return
ticket = await get_ticket(payment.checking_id)
if not ticket:
print("this should never happen", payment)
return
await payment.set_pending(False)
await set_ticket_paid(payment.payment_hash)

View file

@ -0,0 +1,129 @@
<q-expansion-item
group="extras"
icon="swap_vertical_circle"
label="Setup guide"
:content-inset-level="0.5"
>
<q-card>
<q-card-section>
<h5 class="text-subtitle1 q-my-none">
Diagon Alley: Decentralised Market-Stalls
</h5>
<p>
Each Stall has its own keys!<br />
<ol>
<li>Create Shipping Zones you're willing to ship to</li>
<li>Create a Stall to list yiur products on</li>
<li>Create products to put on the Stall</li>
<li>List stalls on a simple frontend shop page, or point at Nostr shop client key</li>
</ol>
Make a list of products to sell, point your list of products at a public
relay. Buyers browse your products on the relay, and pay you directly.
Ratings are managed by the relay. Your stall can be listed in multiple
relays, even over TOR, if you wish to be anonymous.<br />
More information on the
<a href="https://github.com/lnbits/Diagon-Alley"
>Diagon Alley Protocol</a
><br />
<small>
Created by, <a href="https://github.com/benarc">Ben Arc</a></small
>
</p>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
group="extras"
icon="swap_vertical_circle"
label="API info"
:content-inset-level="0.5"
>
<q-expansion-item
group="api"
dense
expand-separator
label="Get prodcuts, categorised by wallet"
>
<q-card>
<q-card-section>
<code
><span class="text-light-blue">GET</span>
/diagonalley/api/v1/stall/products/&lt;relay_id&gt;</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">
Returns 201 CREATED (application/json)
</h5>
<code>Product JSON list</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ request.url_root
}}api/v1/stall/products/&lt;relay_id&gt;</code
>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
group="api"
dense
expand-separator
label="Get invoice for product"
>
<q-card>
<q-card-section>
<code
><span class="text-light-green">POST</span>
/diagonalley/api/v1/stall/order/&lt;relay_id&gt;</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<code
>{"id": &lt;string&gt;, "address": &lt;string&gt;, "shippingzone":
&lt;integer&gt;, "email": &lt;string&gt;, "quantity":
&lt;integer&gt;}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 201 CREATED (application/json)
</h5>
<code
>{"checking_id": &lt;string&gt;,"payment_request":
&lt;string&gt;}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X POST {{ request.url_root
}}api/v1/stall/order/&lt;relay_id&gt; -d '{"id": &lt;product_id&&gt;,
"email": &lt;customer_email&gt;, "address": &lt;customer_address&gt;,
"quantity": 2, "shippingzone": 1}' -H "Content-type: application/json"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
group="api"
dense
expand-separator
label="Check a product has been shipped"
class="q-mb-md"
>
<q-card>
<q-card-section>
<code
><span class="text-light-blue">GET</span>
/diagonalley/api/v1/stall/checkshipped/&lt;checking_id&gt;</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
<code>{"shipped": &lt;boolean&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ request.url_root
}}api/v1/stall/checkshipped/&lt;checking_id&gt; -H "Content-type:
application/json"</code
>
</q-card-section>
</q-card>
</q-expansion-item>
</q-expansion-item>

View file

@ -0,0 +1,634 @@
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {% block page %}
<div class="row q-col-gutter-md">
<q-dialog v-model="productDialog.show" position="top">
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
<q-form @submit="sendProductFormData" class="q-gutter-md">
<q-select
filled
dense
emit-value
v-model="productDialog.data.stall"
:options="stalls"
label="Stall"
>
</q-select>
<q-input
filled
dense
v-model.trim="productDialog.data.product"
label="Product"
></q-input>
<q-input
filled
dense
v-model.trim="productDialog.data.description"
label="Description"
></q-input>
<div class="row">
<div class="col-5">
<q-select
filled
dense
v-model.trim="productDialog.data.categories"
multiple
:options="categories"
label="Categories"
class="q-pr-sm"
></q-select>
</div>
<div class="col-7">
<q-input
filled
dense
v-model.trim="productDialog.categoriesextra"
placeholder="crafts,robots,etc (seperate by comma)"
label="Other categories *optional"
></q-input>
</div>
</div>
<q-file
class="q-pr-md"
filled
dense
capture="environment"
accept="image/jpeg, image/png"
:max-file-size="3*1024**2"
label="Small image (optional)"
clearable
@input="imageAdded"
@clear="imageCleared"
>
<template v-if="productDialog.data.image" v-slot:before>
<img style="height: 1em" :src="productDialog.data.image" />
</template>
<template v-if="productDialog.data.image" v-slot:append>
<q-icon
name="cancel"
@click.stop.prevent="imageCleared"
class="cursor-pointer"
/>
</template>
</q-file>
<q-input
filled
dense
v-model.number="productDialog.data.price"
type="number"
label="Price"
></q-input>
<q-input
filled
dense
v-model.number="productDialog.data.quantity"
type="number"
label="Quantity"
></q-input>
<div class="row q-mt-lg">
<q-btn
v-if="productDialog.data.id"
unelevated
color="primary"
type="submit"
>Update Product</q-btn
>
<q-btn
v-else
unelevated
color="primary"
:disable="productDialog.data.image == null
|| productDialog.data.product == null
|| productDialog.data.description == null
|| productDialog.data.quantity == null"
type="submit"
>Create Product</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="zoneDialog.show" position="top">
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
<q-form @submit="sendZoneFormData" class="q-gutter-md">
<q-select
filled
dense
multiple
:options="shippingZoneOptions"
label="Countries"
v-model.trim="zoneDialog.data.countries"
></q-select>
<q-input
filled
dense
type="number"
v-model.trim="zoneDialog.data.cost"
label="Cost (sats)"
></q-input>
<div class="row q-mt-lg">
<q-btn
v-if="zoneDialog.data.id"
unelevated
color="primary"
type="submit"
>Update Shipping Zone</q-btn
>
<q-btn
v-else
unelevated
color="primary"
:disable="zoneDialog.data.countries == null
|| zoneDialog.data.cost == null"
type="submit"
>Create Shipping Zone</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="shopDialog.show" position="top">
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
<q-form @submit="sendShopFormData" class="q-gutter-md">
<q-toggle
label="Activate shop"
color="primary"
v-model="shopDialog.data.activate"
></q-toggle>
<q-select
filled
dense
multiple
:options="stalls"
label="Stalls"
v-model.trim="shopDialog.data.stalls"
></q-select>
<div class="row q-mt-lg">
<q-btn
v-if="shopDialog.data.id"
unelevated
color="primary"
type="submit"
>Update Relay</q-btn
>
<q-btn
v-else
unelevated
color="primary"
:disable="shopDialog.data.activate == null
|| shopDialog.data.stalls == null"
type="submit"
>Launch</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="stallDialog.show" position="top">
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
<q-form @submit="sendStallFormData" class="q-gutter-md">
<q-input
filled
dense
v-model.trim="stallDialog.data.name"
label="Name"
></q-input>
<q-select
filled
dense
emit-value
v-model="stallDialog.data.wallet"
:options="g.user.walletOptions"
label="Wallet *"
>
</q-select>
<div class="row">
<div class="col-5">
<q-btn unelevated color="primary">Generate keys</q-btn>
</div>
<div class="col-5">
<q-btn unelevated color="primary">Restore keys</q-btn>
</div>
</div>
<q-input
v-if="stallDialog.restorekeys"
filled
dense
v-model.trim="stallDialog.data.publickey"
label="Public Key"
></q-input>
<q-input
v-if="stallDialog.restorekeys"
filled
dense
v-model.trim="stallDialog.data.privatekey"
label="Private Key"
></q-input>
<q-select
:options="shippingZoneOptions"
filled
dense
multiple
v-model.trim="stallDialog.data.shippingzones"
label="Shipping Zones"
></q-select>
<q-select
:options="relayOptions"
filled
dense
multiple
v-model.trim="stallDialog.data.relays"
label="Relays"
></q-select>
<q-input
filled
dense
v-model.trim="stallDialog.data.name"
label="Custom relays (seperate by comma)"
></q-input>
<q-input
filled
dense
v-model.trim="stallDialog.data.nostrShops"
label="Stall public keys (seperate by comma)"
></q-input>
<div class="row q-mt-lg">
<q-btn
v-if="stallDialog.data.id"
unelevated
color="primary"
type="submit"
>Update Stall</q-btn
>
<q-btn
v-else
unelevated
color="primary"
:disable="stallDialog.data.countries == null
|| stallDialog.data.cost == null"
type="submit"
>Create Stall</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
</q-form>
</q-card>
</q-dialog>
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
<q-card>
<q-card-section>
<q-btn unelevated color="primary" @click="productDialog.show = true"
>+ Product <q-tooltip> List a product </q-tooltip></q-btn
>
<q-btn unelevated color="primary" @click="zoneDialog.show = true"
>+ Shipping Zone<q-tooltip> Create a shipping zone </q-tooltip></q-btn
>
<q-btn unelevated color="primary" @click="stallDialog.show = true"
>+ Stall
<q-tooltip> Create a stall to list products on </q-tooltip></q-btn
>
<q-btn unelevated color="primary" @click="shopDialog.show = true"
>Launch frontend shop (not Nostr)
<q-tooltip> Makes a simple frontend shop for your stalls</q-tooltip></q-btn
>
</q-card-section>
</q-card>
<q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-md">
<div class="col">
<h5 class="text-subtitle1 q-my-none">Orders</h5>
</div>
<div class="col-auto">
<q-btn flat color="grey" @click="exportOrdersCSV"
>Export to CSV</q-btn
>
</div>
</div>
<q-table
dense
flat
:data="orders"
row-key="id"
:columns="ordersTable.columns"
:pagination.sync="ordersTable.pagination"
>
{% raw %}
<template v-slot:header="props">
<q-tr :props="props">
<q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label }}
</q-th>
<q-th auto-width></q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.value }}
</q-td>
<q-td auto-width>
<q-btn
flat
dense
size="xs"
@click="shipOrder(props.row.id)"
icon="add_shopping_cart"
color="green"
>
<q-tooltip> Product shipped? </q-tooltip>
</q-btn>
</q-td>
<q-td auto-width>
<q-btn
flat
dense
size="xs"
@click="deleteOrder(props.row.id)"
icon="cancel"
color="pink"
></q-btn>
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</q-card-section>
</q-card>
<q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-md">
<div class="col">
<h5 class="text-subtitle1 q-my-none">Products</h5>
</div>
<div class="col-auto">
<q-btn flat color="grey" @click="exportProductsCSV"
>Export to CSV</q-btn
>
</div>
</div>
<q-table
dense
flat
:data="products"
row-key="id"
:columns="productsTable.columns"
:pagination.sync="productsTable.pagination"
>
{% raw %}
<template v-slot:header="props">
<q-tr :props="props">
<q-th auto-width></q-th>
<q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label }}
</q-th>
<q-th auto-width></q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td auto-width>
<q-btn
unelevated
dense
size="xs"
icon="add_shopping_cart"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
type="a"
:href="props.row.wallet"
target="_blank"
></q-btn>
<q-tooltip> Link to pass to stall relay </q-tooltip>
</q-td>
<q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.value }}
</q-td>
<q-td class="text-center" auto-width>
<img
v-if="props.row.image"
:src="props.row.image"
style="height: 1.5em"
/>
</q-td>
<q-td auto-width>
<q-btn
flat
dense
size="xs"
@click="openProductUpdateDialog(props.row.id)"
icon="edit"
color="light-blue"
></q-btn>
<q-btn
flat
dense
size="xs"
@click="deleteProduct(props.row.id)"
icon="cancel"
color="pink"
></q-btn>
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</q-card-section>
</q-card>
<q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-md">
<div class="col">
<h5 class="text-subtitle1 q-my-none">Stalls</h5>
</div>
<div class="col-auto">
<q-btn flat color="grey" @click="exportStallsCSV"
>Export to CSV</q-btn
>
</div>
</div>
<q-table
dense
flat
:data="stalls"
row-key="id"
:columns="stallTable.columns"
:pagination.sync="stallTable.pagination"
>
{% raw %}
<template v-slot:header="props">
<q-tr :props="props">
<q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label }}
</q-th>
<q-th auto-width></q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.value }}
</q-td>
<q-td auto-width>
<q-btn
flat
dense
size="xs"
@click="openStallUpdateDialog(props.row.id)"
icon="edit"
color="light-blue"
></q-btn>
<q-btn
flat
dense
size="xs"
@click="deleteStall(props.row.id)"
icon="cancel"
color="pink"
></q-btn>
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</q-card-section>
</q-card>
<q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-md">
<div class="col">
<h5 class="text-subtitle1 q-my-none">Shipping Zones</h5>
</div>
<div class="col-auto">
<q-btn flat color="grey" @click="exportZonesCSV"
>Export to CSV</q-btn
>
</div>
</div>
<q-table
dense
flat
:data="zones"
row-key="id"
:columns="zonesTable.columns"
:pagination.sync="zonesTable.pagination"
>
{% raw %}
<template v-slot:header="props">
<q-tr :props="props">
<q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label }}
</q-th>
<q-th auto-width></q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.value }}
</q-td>
<q-td auto-width>
<q-btn
flat
dense
size="xs"
@click="openZoneUpdateDialog(props.row.id)"
icon="edit"
color="light-blue"
></q-btn>
<q-btn
flat
dense
size="xs"
@click="deleteZone(props.row.id)"
icon="cancel"
color="pink"
></q-btn>
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</q-card-section>
</q-card>
</div>
<div class="col-12 col-md-4 col-lg-5 q-gutter-y-md">
<q-card>
<q-card-section>
<h6 class="text-subtitle1 q-my-none">
LNbits Diagon Alley Extension, powered by Nostr
</h6>
</q-card-section>
<q-card-section class="q-pa-none">
<q-separator></q-separator>
<q-list> {% include "diagonalley/_api_docs.html" %} </q-list>
</q-card-section>
</q-card>
<q-card>
<q-card-section>
<h6 class="text-subtitle1 q-my-none">Messages (example)</h6>
</q-card-section>
<q-card-section class="q-pa-none" >
<q-separator></q-separator>
<div class="row q-pa-md">
<div class="col-4">
<q-btn outline color="primary" size="md" style="height: 90px; width:90%"
>OrderID:87h87h<br/>KJBIBYBUYBUF90898....</q-btn
>
<q-btn outline color="primary" size="md" style="height: 90px; width:90%"
>OrderID:NIUHB7<br/>79867KJGJHGVFYFV....</q-btn
>
</div>
<div class="col-8">
<div style="height: 350px">
<q-chat-message
:text="['I have not received my Welsh Cakes']"
sent
></q-chat-message>
<q-chat-message
:text="['Yep, its Brexit. They are stuck in customs. We will have to wait it out. I can offer a full refund?']"
></q-chat-message>
</div>
<q-input ><template v-slot:after>
<q-btn round dense flat icon="send" />
</template></q-input>
</div>
</q-card-section>
</q-card>
</div>
</div>
</div>
{% endblock %} {% block scripts %} {{ window_vars(user) }}
<script src="https://cdn.jsdelivr.net/npm/pica@6.1.1/dist/pica.min.js"></script>
<script src="/diagonalley/static/js/index.js"></script>
{% endblock %}

View file

@ -0,0 +1,9 @@
<pre id="json"></pre>
<script>
document.getElementById('json').innerHTML = JSON.stringify(
'{{ stall }}',
null,
2
)
</script>

View file

@ -0,0 +1,44 @@
from typing import List
from fastapi import Request, WebSocket, WebSocketDisconnect
from fastapi.params import Depends
from fastapi.templating import Jinja2Templates
from starlette.responses import HTMLResponse # type: ignore
from http import HTTPStatus
import json
from lnbits.decorators import check_user_exists, validate_uuids
from lnbits.extensions.diagonalley import diagonalley_ext
from .crud import (
create_diagonalley_product,
get_diagonalley_product,
get_diagonalley_products,
delete_diagonalley_product,
create_diagonalley_order,
get_diagonalley_order,
get_diagonalley_orders,
update_diagonalley_product,
)
@diagonalley_ext.get("/", response_class=HTMLResponse)
@validate_uuids(["usr"], required=True)
@check_user_exists(request: Request)
async def index():
return await render_template("diagonalley/index.html", user=g.user)
@diagonalley_ext.get("/<stall_id>", response_class=HTMLResponse)
async def display(request: Request, stall_id):
product = await get_diagonalley_products(stall_id)
if not product:
abort(HTTPStatus.NOT_FOUND, "Stall does not exist.")
return await render_template(
"diagonalley/stall.html",
stall=json.dumps(
[product._asdict() for product in await get_diagonalley_products(stall_id)]
),
)

View file

@ -0,0 +1,348 @@
from http import HTTPStatus
from fastapi import Request
from fastapi.param_functions import Query
from fastapi.params import Depends
from starlette.exceptions import HTTPException
from lnbits.core.crud import get_user
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
from . import diagonalley_ext
from .crud import (
create_diagonalley_product,
get_diagonalley_product,
get_diagonalley_products,
delete_diagonalley_product,
create_diagonalley_zone,
update_diagonalley_zone,
get_diagonalley_zone,
get_diagonalley_zones,
delete_diagonalley_zone,
create_diagonalley_stall,
update_diagonalley_stall,
get_diagonalley_stall,
get_diagonalley_stalls,
delete_diagonalley_stall,
create_diagonalley_order,
get_diagonalley_order,
get_diagonalley_orders,
update_diagonalley_product,
delete_diagonalley_order,
)
from lnbits.core.services import create_invoice
from base64 import urlsafe_b64encode
from uuid import uuid4
# from lnbits.db import open_ext_db
from . import db
from .models import Products, Orders, Stalls
### Products
@copilot_ext.get("/api/v1/copilot/{copilot_id}")
async def api_copilot_retrieve(
req: Request,
copilot_id: str = Query(None),
wallet: WalletTypeInfo = Depends(get_key_type),
):
copilot = await get_copilot(copilot_id)
if not copilot:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Copilot not found"
)
if not copilot.lnurl_toggle:
return copilot.dict()
return {**copilot.dict(), **{"lnurl": copilot.lnurl(req)}}
@diagonalley_ext.get("/api/v1/products")
async def api_diagonalley_products(
req: Request,
wallet: WalletTypeInfo = Depends(get_key_type),
):
wallet_ids = [wallet.wallet.id]
if "all_stalls" in request.args:
wallet_ids = (await get_user(wallet.wallet.user)).wallet_ids
return ([product._asdict() for product in await get_diagonalley_products(wallet_ids)])
@diagonalley_ext.post("/api/v1/products")
@diagonalley_ext.put("/api/v1/products/{product_id}")
async def api_diagonalley_product_create(
data: Products,
product_id=: str = Query(None),
wallet: WalletTypeInfo = Depends(get_key_type)
):
if product_id:
product = await get_diagonalley_product(product_id)
if not product:
return ({"message": "Withdraw product does not exist."}))
if product.wallet != wallet.wallet.id:
return ({"message": "Not your withdraw product."}))
product = await update_diagonalley_product(product_id, data)
else:
product = await create_diagonalley_product(wallet_id=wallet.wallet.id, data)
return ({**product._asdict()}))
@diagonalley_ext.route("/api/v1/products/{product_id}")
async def api_diagonalley_products_delete(product_id, wallet: WalletTypeInfo = Depends(require_admin_key)):
product = await get_diagonalley_product(product_id)
if not product:
return ({"message": "Product does not exist."})
if product.wallet != wallet.wallet.id:
return ({"message": "Not your Diagon Alley."})
await delete_diagonalley_product(product_id)
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
# # # Shippingzones
@diagonalley_ext.get("/api/v1/zones")
async def api_diagonalley_zones(wallet: WalletTypeInfo = Depends(get_key_type)):
wallet_ids = [wallet.wallet.id]
if "all_wallets" in request.args:
wallet_ids = (await get_user(wallet.wallet.user)).wallet_ids
return ([zone._asdict() for zone in await get_diagonalley_zones(wallet_ids)]))
@diagonalley_ext.post("/api/v1/zones")
@diagonalley_ext.put("/api/v1/zones/{zone_id}")
async def api_diagonalley_zone_create(
data: Zones,
zone_id: str = Query(None),
wallet: WalletTypeInfo = Depends(get_key_type)
):
if zone_id:
zone = await get_diagonalley_zone(zone_id)
if not zone:
return ({"message": "Zone does not exist."}))
if zone.wallet != walley.wallet.id:
return ({"message": "Not your record."}))
zone = await update_diagonalley_zone(zone_id, data)
else:
zone = await create_diagonalley_zone(wallet=wallet.wallet.id, data)
return ({**zone._asdict()}))
@diagonalley_ext.delete("/api/v1/zones/{zone_id}")
async def api_diagonalley_zone_delete(zone_id: str = Query(None), wallet: WalletTypeInfo = Depends(require_admin_key)):
zone = await get_diagonalley_zone(zone_id)
if not zone:
return ({"message": "zone does not exist."})
if zone.wallet != wallet.wallet.id:
return ({"message": "Not your zone."})
await delete_diagonalley_zone(zone_id)
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
# # # Stalls
@diagonalley_ext.get("/api/v1/stalls")
async def api_diagonalley_stalls(wallet: WalletTypeInfo = Depends(get_key_type)):
wallet_ids = [wallet.wallet.id]
if "all_wallets" in request.args:
wallet_ids = (await get_user(wallet.wallet.user)).wallet_ids
return ([stall._asdict() for stall in await get_diagonalley_stalls(wallet_ids)])
@diagonalley_ext.post("/api/v1/stalls")
@diagonalley_ext.put("/api/v1/stalls/{stall_id}")
async def api_diagonalley_stall_create(data: createStalls, stall_id: str = Query(None), wallet: WalletTypeInfo = Depends(get_key_type)):
if stall_id:
stall = await get_diagonalley_stall(stall_id)
if not stall:
return ({"message": "Withdraw stall does not exist."}))
if stall.wallet != wallet.wallet.id:
return ({"message": "Not your withdraw stall."}))
stall = await update_diagonalley_stall(stall_id, data)
else:
stall = await create_diagonalley_stall(wallet_id=wallet.wallet.id, data)
return ({**stall._asdict()}))
@diagonalley_ext.delete("/api/v1/stalls/{stall_id}")
async def api_diagonalley_stall_delete(stall_id: str = Query(None), wallet: WalletTypeInfo = Depends(require_admin_key)):
stall = await get_diagonalley_stall(stall_id)
if not stall:
return ({"message": "Stall does not exist."})
if stall.wallet != wallet.wallet.id:
return ({"message": "Not your Stall."})
await delete_diagonalley_stall(stall_id)
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
###Orders
@diagonalley_ext.get("/api/v1/orders")
async def api_diagonalley_orders(wallet: WalletTypeInfo = Depends(get_key_type)):
wallet_ids = [wallet.wallet.id]
if "all_wallets" in request.args:
wallet_ids = (await get_user(wallet.wallet.user)).wallet_ids
try:
return ([order._asdict() for order in await get_diagonalley_orders(wallet_ids)])
except:
return ({"message": "We could not retrieve the orders."}))
@diagonalley_ext.post("/api/v1/orders")
async def api_diagonalley_order_create(data: createOrders, wallet: WalletTypeInfo = Depends(get_key_type)):
order = await create_diagonalley_order(wallet_id=wallet.wallet.id, data)
return ({**order._asdict()})
@diagonalley_ext.delete("/api/v1/orders/{order_id}")
async def api_diagonalley_order_delete(order_id: str = Query(None), wallet: WalletTypeInfo = Depends(get_key_type)):
order = await get_diagonalley_order(order_id)
if not order:
return ({"message": "Order does not exist."})
if order.wallet != wallet.wallet.id:
return ({"message": "Not your Order."})
await delete_diagonalley_order(order_id)
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
@diagonalley_ext.get("/api/v1/orders/paid/{order_id}")
async def api_diagonalley_order_paid(order_id: str = Query(None), wallet: WalletTypeInfo = Depends(require_admin_key)):
await db.execute(
"UPDATE diagonalley.orders SET paid = ? WHERE id = ?",
(
True,
order_id,
),
)
return "", HTTPStatus.OK
@diagonalley_ext.get("/api/v1/orders/shipped/{order_id}")
async def api_diagonalley_order_shipped(order_id: str = Query(None), wallet: WalletTypeInfo = Depends(get_key_type)):
await db.execute(
"UPDATE diagonalley.orders SET shipped = ? WHERE id = ?",
(
True,
order_id,
),
)
order = await db.fetchone(
"SELECT * FROM diagonalley.orders WHERE id = ?", (order_id,)
)
return ([order._asdict() for order in get_diagonalley_orders(order["wallet"])]))
###List products based on stall id
@diagonalley_ext.get("/api/v1/stall/products/{stall_id}")
async def api_diagonalley_stall_products(stall_id: str = Query(None), wallet: WalletTypeInfo = Depends(get_key_type)):
rows = await db.fetchone(
"SELECT * FROM diagonalley.stalls WHERE id = ?", (stall_id,)
)
print(rows[1])
if not rows:
return ({"message": "Stall does not exist."})
products = db.fetchone(
"SELECT * FROM diagonalley.products WHERE wallet = ?", (rows[1],)
)
if not products:
return ({"message": "No products"})
return ([products._asdict() for products in await get_diagonalley_products(rows[1])])
###Check a product has been shipped
@diagonalley_ext.get("/api/v1/stall/checkshipped/{checking_id}")
async def api_diagonalley_stall_checkshipped(checking_id: str = Query(None), wallet: WalletTypeInfo = Depends(get_key_type)):
rows = await db.fetchone(
"SELECT * FROM diagonalley.orders WHERE invoiceid = ?", (checking_id,)
)
return ({"shipped": rows["shipped"]})
###Place order
@diagonalley_ext.post("/api/v1/stall/order/{stall_id}")
async def api_diagonalley_stall_order(data:createOrders, wallet: WalletTypeInfo = Depends(get_key_type)):
product = await get_diagonalley_product(data.id)
shipping = await get_diagonalley_stall(stall_id)
if data.shippingzone == 1:
shippingcost = shipping.zone1cost
else:
shippingcost = shipping.zone2cost
checking_id, payment_request = await create_invoice(
wallet_id=product.wallet,
amount=shippingcost + (data.quantity * product.price),
memo=data.id,
)
selling_id = urlsafe_b64encode(uuid4().bytes_le).decode("utf-8")
await db.execute(
"""
INSERT INTO diagonalley.orders (id, productid, wallet, product, quantity, shippingzone, address, email, invoiceid, paid, shipped)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
selling_id,
data.id,
product.wallet,
product.product,
data.quantity,
data.shippingzone,
data.address,
data.email,
checking_id,
False,
False,
),
)
return ({"checking_id": checking_id, "payment_request": payment_request}))