mirror of
https://github.com/lnbits/lnbits-legend.git
synced 2025-02-25 07:07:48 +01:00
initial
This commit is contained in:
parent
f4580955b9
commit
b1bfef8784
13 changed files with 2569 additions and 0 deletions
9
lnbits/extensions/diagonalley/README.md
Normal file
9
lnbits/extensions/diagonalley/README.md
Normal 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>
|
16
lnbits/extensions/diagonalley/__init__.py
Normal file
16
lnbits/extensions/diagonalley/__init__.py
Normal 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))
|
6
lnbits/extensions/diagonalley/config.json
Normal file
6
lnbits/extensions/diagonalley/config.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "Diagon Alley",
|
||||
"short_description": "Movable anonymous market stand",
|
||||
"icon": "add_shopping_cart",
|
||||
"contributors": ["benarc","DeanH"]
|
||||
}
|
395
lnbits/extensions/diagonalley/crud.py
Normal file
395
lnbits/extensions/diagonalley/crud.py
Normal 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,))
|
69
lnbits/extensions/diagonalley/migrations.py
Normal file
69
lnbits/extensions/diagonalley/migrations.py
Normal 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
|
||||
);
|
||||
"""
|
||||
)
|
57
lnbits/extensions/diagonalley/models.py
Normal file
57
lnbits/extensions/diagonalley/models.py
Normal 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
|
824
lnbits/extensions/diagonalley/static/js/index.js
Normal file
824
lnbits/extensions/diagonalley/static/js/index.js
Normal 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()
|
||||
}
|
||||
}
|
||||
})
|
29
lnbits/extensions/diagonalley/tasks.py
Normal file
29
lnbits/extensions/diagonalley/tasks.py
Normal 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)
|
|
@ -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/<relay_id></code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
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/<relay_id></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/<relay_id></code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
||||
<code
|
||||
>{"id": <string>, "address": <string>, "shippingzone":
|
||||
<integer>, "email": <string>, "quantity":
|
||||
<integer>}</code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 201 CREATED (application/json)
|
||||
</h5>
|
||||
<code
|
||||
>{"checking_id": <string>,"payment_request":
|
||||
<string>}</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/<relay_id> -d '{"id": <product_id&>,
|
||||
"email": <customer_email>, "address": <customer_address>,
|
||||
"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/<checking_id></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": <boolean>}</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/<checking_id> -H "Content-type:
|
||||
application/json"</code
|
||||
>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
</q-expansion-item>
|
634
lnbits/extensions/diagonalley/templates/diagonalley/index.html
Normal file
634
lnbits/extensions/diagonalley/templates/diagonalley/index.html
Normal 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 %}
|
|
@ -0,0 +1,9 @@
|
|||
<pre id="json"></pre>
|
||||
|
||||
<script>
|
||||
document.getElementById('json').innerHTML = JSON.stringify(
|
||||
'{{ stall }}',
|
||||
null,
|
||||
2
|
||||
)
|
||||
</script>
|
44
lnbits/extensions/diagonalley/views.py
Normal file
44
lnbits/extensions/diagonalley/views.py
Normal 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)]
|
||||
),
|
||||
)
|
348
lnbits/extensions/diagonalley/views_api.py
Normal file
348
lnbits/extensions/diagonalley/views_api.py
Normal 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}))
|
Loading…
Add table
Reference in a new issue