commit
1660ef6ef8
20 changed files with 997 additions and 430 deletions
|
|
@ -42,9 +42,13 @@ from .views_api import * # noqa
|
|||
|
||||
def nostrmarket_start():
|
||||
async def _subscribe_to_nostr_client():
|
||||
# wait for 'nostrclient' extension to initialize
|
||||
await asyncio.sleep(10)
|
||||
await subscribe_to_nostr_client(recieve_event_queue, send_req_queue)
|
||||
|
||||
async def _wait_for_nostr_events():
|
||||
# wait for this extension to initialize
|
||||
await asyncio.sleep(5)
|
||||
await wait_for_nostr_events(recieve_event_queue, send_req_queue)
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
|
|
|
|||
301
crud.py
301
crud.py
|
|
@ -5,8 +5,10 @@ from lnbits.helpers import urlsafe_short_hash
|
|||
|
||||
from . import db
|
||||
from .models import (
|
||||
DirectMessage,
|
||||
Merchant,
|
||||
Order,
|
||||
PartialDirectMessage,
|
||||
PartialMerchant,
|
||||
PartialProduct,
|
||||
PartialStall,
|
||||
|
|
@ -71,19 +73,26 @@ async def get_merchant_for_user(user_id: str) -> Optional[Merchant]:
|
|||
return Merchant.from_row(row) if row else None
|
||||
|
||||
|
||||
async def delete_merchants(merchant_id: str) -> None:
|
||||
await db.execute(
|
||||
"DELETE FROM nostrmarket.merchants WHERE id = ?",
|
||||
(merchant_id,),
|
||||
)
|
||||
|
||||
|
||||
######################################## ZONES ########################################
|
||||
|
||||
|
||||
async def create_zone(user_id: str, data: PartialZone) -> Zone:
|
||||
async def create_zone(merchant_id: str, data: PartialZone) -> Zone:
|
||||
zone_id = urlsafe_short_hash()
|
||||
await db.execute(
|
||||
f"""
|
||||
INSERT INTO nostrmarket.zones (id, user_id, name, currency, cost, regions)
|
||||
INSERT INTO nostrmarket.zones (id, merchant_id, name, currency, cost, regions)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
zone_id,
|
||||
user_id,
|
||||
merchant_id,
|
||||
data.name,
|
||||
data.currency,
|
||||
data.cost,
|
||||
|
|
@ -91,55 +100,67 @@ async def create_zone(user_id: str, data: PartialZone) -> Zone:
|
|||
),
|
||||
)
|
||||
|
||||
zone = await get_zone(user_id, zone_id)
|
||||
zone = await get_zone(merchant_id, zone_id)
|
||||
assert zone, "Newly created zone couldn't be retrieved"
|
||||
return zone
|
||||
|
||||
|
||||
async def update_zone(user_id: str, z: Zone) -> Optional[Zone]:
|
||||
async def update_zone(merchant_id: str, z: Zone) -> Optional[Zone]:
|
||||
await db.execute(
|
||||
f"UPDATE nostrmarket.zones SET name = ?, cost = ?, regions = ? WHERE id = ? AND user_id = ?",
|
||||
(z.name, z.cost, json.dumps(z.countries), z.id, user_id),
|
||||
f"UPDATE nostrmarket.zones SET name = ?, cost = ?, regions = ? WHERE id = ? AND merchant_id = ?",
|
||||
(z.name, z.cost, json.dumps(z.countries), z.id, merchant_id),
|
||||
)
|
||||
return await get_zone(user_id, z.id)
|
||||
return await get_zone(merchant_id, z.id)
|
||||
|
||||
|
||||
async def get_zone(user_id: str, zone_id: str) -> Optional[Zone]:
|
||||
async def get_zone(merchant_id: str, zone_id: str) -> Optional[Zone]:
|
||||
row = await db.fetchone(
|
||||
"SELECT * FROM nostrmarket.zones WHERE user_id = ? AND id = ?",
|
||||
"SELECT * FROM nostrmarket.zones WHERE merchant_id = ? AND id = ?",
|
||||
(
|
||||
user_id,
|
||||
merchant_id,
|
||||
zone_id,
|
||||
),
|
||||
)
|
||||
return Zone.from_row(row) if row else None
|
||||
|
||||
|
||||
async def get_zones(user_id: str) -> List[Zone]:
|
||||
async def get_zones(merchant_id: str) -> List[Zone]:
|
||||
rows = await db.fetchall(
|
||||
"SELECT * FROM nostrmarket.zones WHERE user_id = ?", (user_id,)
|
||||
"SELECT * FROM nostrmarket.zones WHERE merchant_id = ?", (merchant_id,)
|
||||
)
|
||||
return [Zone.from_row(row) for row in rows]
|
||||
|
||||
|
||||
async def delete_zone(zone_id: str) -> None:
|
||||
# todo: add user_id
|
||||
await db.execute("DELETE FROM nostrmarket.zones WHERE id = ?", (zone_id,))
|
||||
async def delete_zone(merchant_id: str, zone_id: str) -> None:
|
||||
|
||||
await db.execute(
|
||||
"DELETE FROM nostrmarket.zones WHERE merchant_id = ? AND id = ?",
|
||||
(
|
||||
merchant_id,
|
||||
zone_id,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def delete_merchant_zones(merchant_id: str) -> None:
|
||||
await db.execute(
|
||||
"DELETE FROM nostrmarket.zones WHERE merchant_id = ?", (merchant_id,)
|
||||
)
|
||||
|
||||
|
||||
######################################## STALL ########################################
|
||||
|
||||
|
||||
async def create_stall(user_id: str, data: PartialStall) -> Stall:
|
||||
async def create_stall(merchant_id: str, data: PartialStall) -> Stall:
|
||||
stall_id = urlsafe_short_hash()
|
||||
|
||||
await db.execute(
|
||||
f"""
|
||||
INSERT INTO nostrmarket.stalls (user_id, id, wallet, name, currency, zones, meta)
|
||||
INSERT INTO nostrmarket.stalls (merchant_id, id, wallet, name, currency, zones, meta)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
user_id,
|
||||
merchant_id,
|
||||
stall_id,
|
||||
data.wallet,
|
||||
data.name,
|
||||
|
|
@ -151,35 +172,35 @@ async def create_stall(user_id: str, data: PartialStall) -> Stall:
|
|||
),
|
||||
)
|
||||
|
||||
stall = await get_stall(user_id, stall_id)
|
||||
stall = await get_stall(merchant_id, stall_id)
|
||||
assert stall, "Newly created stall couldn't be retrieved"
|
||||
return stall
|
||||
|
||||
|
||||
async def get_stall(user_id: str, stall_id: str) -> Optional[Stall]:
|
||||
async def get_stall(merchant_id: str, stall_id: str) -> Optional[Stall]:
|
||||
row = await db.fetchone(
|
||||
"SELECT * FROM nostrmarket.stalls WHERE user_id = ? AND id = ?",
|
||||
"SELECT * FROM nostrmarket.stalls WHERE merchant_id = ? AND id = ?",
|
||||
(
|
||||
user_id,
|
||||
merchant_id,
|
||||
stall_id,
|
||||
),
|
||||
)
|
||||
return Stall.from_row(row) if row else None
|
||||
|
||||
|
||||
async def get_stalls(user_id: str) -> List[Stall]:
|
||||
async def get_stalls(merchant_id: str) -> List[Stall]:
|
||||
rows = await db.fetchall(
|
||||
"SELECT * FROM nostrmarket.stalls WHERE user_id = ?",
|
||||
(user_id,),
|
||||
"SELECT * FROM nostrmarket.stalls WHERE merchant_id = ?",
|
||||
(merchant_id,),
|
||||
)
|
||||
return [Stall.from_row(row) for row in rows]
|
||||
|
||||
|
||||
async def update_stall(user_id: str, stall: Stall) -> Optional[Stall]:
|
||||
async def update_stall(merchant_id: str, stall: Stall) -> Optional[Stall]:
|
||||
await db.execute(
|
||||
f"""
|
||||
UPDATE nostrmarket.stalls SET wallet = ?, name = ?, currency = ?, zones = ?, meta = ?
|
||||
WHERE user_id = ? AND id = ?
|
||||
WHERE merchant_id = ? AND id = ?
|
||||
""",
|
||||
(
|
||||
stall.wallet,
|
||||
|
|
@ -189,36 +210,43 @@ async def update_stall(user_id: str, stall: Stall) -> Optional[Stall]:
|
|||
[z.dict() for z in stall.shipping_zones]
|
||||
), # todo: cost is float. should be int for sats
|
||||
json.dumps(stall.config.dict()),
|
||||
user_id,
|
||||
merchant_id,
|
||||
stall.id,
|
||||
),
|
||||
)
|
||||
return await get_stall(user_id, stall.id)
|
||||
return await get_stall(merchant_id, stall.id)
|
||||
|
||||
|
||||
async def delete_stall(user_id: str, stall_id: str) -> None:
|
||||
async def delete_stall(merchant_id: str, stall_id: str) -> None:
|
||||
await db.execute(
|
||||
"DELETE FROM nostrmarket.stalls WHERE user_id =? AND id = ?",
|
||||
"DELETE FROM nostrmarket.stalls WHERE merchant_id =? AND id = ?",
|
||||
(
|
||||
user_id,
|
||||
merchant_id,
|
||||
stall_id,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def delete_merchant_stalls(merchant_id: str) -> None:
|
||||
await db.execute(
|
||||
"DELETE FROM nostrmarket.stalls WHERE merchant_id = ?",
|
||||
(merchant_id,),
|
||||
)
|
||||
|
||||
|
||||
######################################## PRODUCTS ########################################
|
||||
|
||||
|
||||
async def create_product(user_id: str, data: PartialProduct) -> Product:
|
||||
async def create_product(merchant_id: str, data: PartialProduct) -> Product:
|
||||
product_id = urlsafe_short_hash()
|
||||
|
||||
await db.execute(
|
||||
f"""
|
||||
INSERT INTO nostrmarket.products (user_id, id, stall_id, name, image, price, quantity, category_list, meta)
|
||||
INSERT INTO nostrmarket.products (merchant_id, id, stall_id, name, image, price, quantity, category_list, meta)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
user_id,
|
||||
merchant_id,
|
||||
product_id,
|
||||
data.stall_id,
|
||||
data.name,
|
||||
|
|
@ -229,18 +257,18 @@ async def create_product(user_id: str, data: PartialProduct) -> Product:
|
|||
json.dumps(data.config.dict()),
|
||||
),
|
||||
)
|
||||
product = await get_product(user_id, product_id)
|
||||
product = await get_product(merchant_id, product_id)
|
||||
assert product, "Newly created product couldn't be retrieved"
|
||||
|
||||
return product
|
||||
|
||||
|
||||
async def update_product(user_id: str, product: Product) -> Product:
|
||||
async def update_product(merchant_id: str, product: Product) -> Product:
|
||||
|
||||
await db.execute(
|
||||
f"""
|
||||
UPDATE nostrmarket.products set name = ?, image = ?, price = ?, quantity = ?, category_list = ?, meta = ?
|
||||
WHERE user_id = ? AND id = ?
|
||||
WHERE merchant_id = ? AND id = ?
|
||||
""",
|
||||
(
|
||||
product.name,
|
||||
|
|
@ -249,40 +277,42 @@ async def update_product(user_id: str, product: Product) -> Product:
|
|||
product.quantity,
|
||||
json.dumps(product.categories),
|
||||
json.dumps(product.config.dict()),
|
||||
user_id,
|
||||
merchant_id,
|
||||
product.id,
|
||||
),
|
||||
)
|
||||
updated_product = await get_product(user_id, product.id)
|
||||
updated_product = await get_product(merchant_id, product.id)
|
||||
assert updated_product, "Updated product couldn't be retrieved"
|
||||
|
||||
return updated_product
|
||||
|
||||
|
||||
async def get_product(user_id: str, product_id: str) -> Optional[Product]:
|
||||
async def get_product(merchant_id: str, product_id: str) -> Optional[Product]:
|
||||
row = await db.fetchone(
|
||||
"SELECT * FROM nostrmarket.products WHERE user_id =? AND id = ?",
|
||||
"SELECT * FROM nostrmarket.products WHERE merchant_id =? AND id = ?",
|
||||
(
|
||||
user_id,
|
||||
merchant_id,
|
||||
product_id,
|
||||
),
|
||||
)
|
||||
return Product.from_row(row) if row else None
|
||||
|
||||
|
||||
async def get_products(user_id: str, stall_id: str) -> List[Product]:
|
||||
async def get_products(merchant_id: str, stall_id: str) -> List[Product]:
|
||||
rows = await db.fetchall(
|
||||
"SELECT * FROM nostrmarket.products WHERE user_id = ? AND stall_id = ?",
|
||||
(user_id, stall_id),
|
||||
"SELECT * FROM nostrmarket.products WHERE merchant_id = ? AND stall_id = ?",
|
||||
(merchant_id, stall_id),
|
||||
)
|
||||
return [Product.from_row(row) for row in rows]
|
||||
|
||||
|
||||
async def get_products_by_ids(user_id: str, product_ids: List[str]) -> List[Product]:
|
||||
async def get_products_by_ids(
|
||||
merchant_id: str, product_ids: List[str]
|
||||
) -> List[Product]:
|
||||
q = ",".join(["?"] * len(product_ids))
|
||||
rows = await db.fetchall(
|
||||
f"SELECT id, stall_id, name, price, quantity, category_list, meta FROM nostrmarket.products WHERE user_id = ? AND id IN ({q})",
|
||||
(user_id, *product_ids),
|
||||
f"SELECT id, stall_id, name, price, quantity, category_list, meta FROM nostrmarket.products WHERE merchant_id = ? AND id IN ({q})",
|
||||
(merchant_id, *product_ids),
|
||||
)
|
||||
return [Product.from_row(row) for row in rows]
|
||||
|
||||
|
|
@ -300,30 +330,54 @@ async def get_wallet_for_product(product_id: str) -> Optional[str]:
|
|||
return row[0] if row else None
|
||||
|
||||
|
||||
async def delete_product(user_id: str, product_id: str) -> None:
|
||||
async def delete_product(merchant_id: str, product_id: str) -> None:
|
||||
await db.execute(
|
||||
"DELETE FROM nostrmarket.products WHERE user_id =? AND id = ?",
|
||||
"DELETE FROM nostrmarket.products WHERE merchant_id =? AND id = ?",
|
||||
(
|
||||
user_id,
|
||||
merchant_id,
|
||||
product_id,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def delete_merchant_products(merchant_id: str) -> None:
|
||||
await db.execute(
|
||||
"DELETE FROM nostrmarket.products WHERE merchant_id = ?",
|
||||
(merchant_id,),
|
||||
)
|
||||
|
||||
|
||||
######################################## ORDERS ########################################
|
||||
|
||||
|
||||
async def create_order(user_id: str, o: Order) -> Order:
|
||||
async def create_order(merchant_id: str, o: Order) -> Order:
|
||||
await db.execute(
|
||||
f"""
|
||||
INSERT INTO nostrmarket.orders (user_id, id, event_id, pubkey, address, contact_data, extra_data, order_items, stall_id, invoice_id, total)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO nostrmarket.orders (
|
||||
merchant_id,
|
||||
id,
|
||||
event_id,
|
||||
event_created_at,
|
||||
merchant_public_key,
|
||||
public_key,
|
||||
address,
|
||||
contact_data,
|
||||
extra_data,
|
||||
order_items,
|
||||
stall_id,
|
||||
invoice_id,
|
||||
total
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(event_id) DO NOTHING
|
||||
""",
|
||||
(
|
||||
user_id,
|
||||
merchant_id,
|
||||
o.id,
|
||||
o.event_id,
|
||||
o.pubkey,
|
||||
o.event_created_at,
|
||||
o.merchant_public_key,
|
||||
o.public_key,
|
||||
o.address,
|
||||
json.dumps(o.contact.dict() if o.contact else {}),
|
||||
json.dumps(o.extra.dict()),
|
||||
|
|
@ -333,53 +387,64 @@ async def create_order(user_id: str, o: Order) -> Order:
|
|||
o.total,
|
||||
),
|
||||
)
|
||||
order = await get_order(user_id, o.id)
|
||||
order = await get_order(merchant_id, o.id)
|
||||
assert order, "Newly created order couldn't be retrieved"
|
||||
|
||||
return order
|
||||
|
||||
|
||||
async def get_order(user_id: str, order_id: str) -> Optional[Order]:
|
||||
async def get_order(merchant_id: str, order_id: str) -> Optional[Order]:
|
||||
row = await db.fetchone(
|
||||
"SELECT * FROM nostrmarket.orders WHERE user_id =? AND id = ?",
|
||||
"SELECT * FROM nostrmarket.orders WHERE merchant_id =? AND id = ?",
|
||||
(
|
||||
user_id,
|
||||
merchant_id,
|
||||
order_id,
|
||||
),
|
||||
)
|
||||
return Order.from_row(row) if row else None
|
||||
|
||||
|
||||
async def get_order_by_event_id(user_id: str, event_id: str) -> Optional[Order]:
|
||||
async def get_order_by_event_id(merchant_id: str, event_id: str) -> Optional[Order]:
|
||||
row = await db.fetchone(
|
||||
"SELECT * FROM nostrmarket.orders WHERE user_id =? AND event_id =?",
|
||||
"SELECT * FROM nostrmarket.orders WHERE merchant_id =? AND event_id =?",
|
||||
(
|
||||
user_id,
|
||||
merchant_id,
|
||||
event_id,
|
||||
),
|
||||
)
|
||||
return Order.from_row(row) if row else None
|
||||
|
||||
|
||||
async def get_orders(user_id: str) -> List[Order]:
|
||||
async def get_orders(merchant_id: str) -> List[Order]:
|
||||
rows = await db.fetchall(
|
||||
"SELECT * FROM nostrmarket.orders WHERE user_id = ? ORDER BY time DESC",
|
||||
(user_id,),
|
||||
"SELECT * FROM nostrmarket.orders WHERE merchant_id = ? ORDER BY time DESC",
|
||||
(merchant_id,),
|
||||
)
|
||||
return [Order.from_row(row) for row in rows]
|
||||
|
||||
|
||||
async def get_orders_for_stall(user_id: str, stall_id: str) -> List[Order]:
|
||||
async def get_orders_for_stall(merchant_id: str, stall_id: str) -> List[Order]:
|
||||
rows = await db.fetchall(
|
||||
"SELECT * FROM nostrmarket.orders WHERE user_id = ? AND stall_id = ? ORDER BY time DESC",
|
||||
"SELECT * FROM nostrmarket.orders WHERE merchant_id = ? AND stall_id = ? ORDER BY time DESC",
|
||||
(
|
||||
user_id,
|
||||
merchant_id,
|
||||
stall_id,
|
||||
),
|
||||
)
|
||||
return [Order.from_row(row) for row in rows]
|
||||
|
||||
|
||||
async def get_last_order_time(public_key: str) -> int:
|
||||
row = await db.fetchone(
|
||||
"""
|
||||
SELECT event_created_at FROM nostrmarket.orders
|
||||
WHERE merchant_public_key = ? ORDER BY event_created_at DESC LIMIT 1
|
||||
""",
|
||||
(public_key,),
|
||||
)
|
||||
return row[0] if row else 0
|
||||
|
||||
|
||||
async def update_order_paid_status(order_id: str, paid: bool) -> Optional[Order]:
|
||||
await db.execute(
|
||||
f"UPDATE nostrmarket.orders SET paid = ? WHERE id = ?",
|
||||
|
|
@ -393,11 +458,11 @@ async def update_order_paid_status(order_id: str, paid: bool) -> Optional[Order]
|
|||
|
||||
|
||||
async def update_order_shipped_status(
|
||||
user_id: str, order_id: str, shipped: bool
|
||||
merchant_id: str, order_id: str, shipped: bool
|
||||
) -> Optional[Order]:
|
||||
await db.execute(
|
||||
f"UPDATE nostrmarket.orders SET shipped = ? WHERE user_id = ? AND id = ?",
|
||||
(shipped, user_id, order_id),
|
||||
f"UPDATE nostrmarket.orders SET shipped = ? WHERE merchant_id = ? AND id = ?",
|
||||
(shipped, merchant_id, order_id),
|
||||
)
|
||||
|
||||
row = await db.fetchone(
|
||||
|
|
@ -405,3 +470,91 @@ async def update_order_shipped_status(
|
|||
(order_id,),
|
||||
)
|
||||
return Order.from_row(row) if row else None
|
||||
|
||||
|
||||
async def delete_merchant_orders(merchant_id: str) -> None:
|
||||
await db.execute(
|
||||
"DELETE FROM nostrmarket.orders WHERE merchant_id = ?",
|
||||
(merchant_id,),
|
||||
)
|
||||
|
||||
|
||||
######################################## MESSAGES ########################################L
|
||||
|
||||
|
||||
async def create_direct_message(
|
||||
merchant_id: str, dm: PartialDirectMessage
|
||||
) -> DirectMessage:
|
||||
dm_id = urlsafe_short_hash()
|
||||
await db.execute(
|
||||
f"""
|
||||
INSERT INTO nostrmarket.direct_messages (merchant_id, id, event_id, event_created_at, message, public_key, incoming)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(event_id) DO NOTHING
|
||||
""",
|
||||
(
|
||||
merchant_id,
|
||||
dm_id,
|
||||
dm.event_id,
|
||||
dm.event_created_at,
|
||||
dm.message,
|
||||
dm.public_key,
|
||||
dm.incoming,
|
||||
),
|
||||
)
|
||||
if dm.event_id:
|
||||
msg = await get_direct_message_by_event_id(merchant_id, dm.event_id)
|
||||
else:
|
||||
msg = await get_direct_message(merchant_id, dm_id)
|
||||
assert msg, "Newly created dm couldn't be retrieved"
|
||||
return msg
|
||||
|
||||
|
||||
async def get_direct_message(merchant_id: str, dm_id: str) -> Optional[DirectMessage]:
|
||||
row = await db.fetchone(
|
||||
"SELECT * FROM nostrmarket.direct_messages WHERE merchant_id = ? AND id = ?",
|
||||
(
|
||||
merchant_id,
|
||||
dm_id,
|
||||
),
|
||||
)
|
||||
return DirectMessage.from_row(row) if row else None
|
||||
|
||||
|
||||
async def get_direct_message_by_event_id(
|
||||
merchant_id: str, event_id: str
|
||||
) -> Optional[DirectMessage]:
|
||||
row = await db.fetchone(
|
||||
"SELECT * FROM nostrmarket.direct_messages WHERE merchant_id = ? AND event_id = ?",
|
||||
(
|
||||
merchant_id,
|
||||
event_id,
|
||||
),
|
||||
)
|
||||
return DirectMessage.from_row(row) if row else None
|
||||
|
||||
|
||||
async def get_direct_messages(merchant_id: str, public_key: str) -> List[DirectMessage]:
|
||||
rows = await db.fetchall(
|
||||
"SELECT * FROM nostrmarket.direct_messages WHERE merchant_id = ? AND public_key = ? ORDER BY event_created_at",
|
||||
(merchant_id, public_key),
|
||||
)
|
||||
return [DirectMessage.from_row(row) for row in rows]
|
||||
|
||||
|
||||
async def get_last_direct_messages_time(public_key: str) -> int:
|
||||
row = await db.fetchone(
|
||||
"""
|
||||
SELECT event_created_at FROM nostrmarket.direct_messages
|
||||
WHERE public_key = ? ORDER BY event_created_at DESC LIMIT 1
|
||||
""",
|
||||
(public_key,),
|
||||
)
|
||||
return row[0] if row else 0
|
||||
|
||||
|
||||
async def delete_merchant_direct_messages(merchant_id: str) -> None:
|
||||
await db.execute(
|
||||
"DELETE FROM nostrmarket.direct_messages WHERE merchant_id = ?",
|
||||
(merchant_id,),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ def get_shared_secret(privkey: str, pubkey: str):
|
|||
|
||||
def decrypt_message(encoded_message: str, encryption_key) -> str:
|
||||
encoded_data = encoded_message.split("?iv=")
|
||||
if len(encoded_data) == 1:
|
||||
return encoded_data[0]
|
||||
encoded_content, encoded_iv = encoded_data[0], encoded_data[1]
|
||||
|
||||
iv = base64.b64decode(encoded_iv)
|
||||
|
|
|
|||
|
|
@ -18,11 +18,11 @@ async def m001_initial(db):
|
|||
"""
|
||||
Initial stalls table.
|
||||
"""
|
||||
# user_id, id, wallet, name, currency, zones, meta
|
||||
|
||||
await db.execute(
|
||||
"""
|
||||
CREATE TABLE nostrmarket.stalls (
|
||||
user_id TEXT NOT NULL,
|
||||
merchant_id TEXT NOT NULL,
|
||||
id TEXT PRIMARY KEY,
|
||||
wallet TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
|
|
@ -39,7 +39,7 @@ async def m001_initial(db):
|
|||
await db.execute(
|
||||
"""
|
||||
CREATE TABLE nostrmarket.products (
|
||||
user_id TEXT NOT NULL,
|
||||
merchant_id TEXT NOT NULL,
|
||||
id TEXT PRIMARY KEY,
|
||||
stall_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
|
|
@ -59,7 +59,7 @@ async def m001_initial(db):
|
|||
"""
|
||||
CREATE TABLE nostrmarket.zones (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
merchant_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
currency TEXT NOT NULL,
|
||||
cost REAL NOT NULL,
|
||||
|
|
@ -75,10 +75,12 @@ async def m001_initial(db):
|
|||
await db.execute(
|
||||
f"""
|
||||
CREATE TABLE nostrmarket.orders (
|
||||
user_id TEXT NOT NULL,
|
||||
merchant_id TEXT NOT NULL,
|
||||
id TEXT PRIMARY KEY,
|
||||
event_id TEXT,
|
||||
pubkey TEXT NOT NULL,
|
||||
event_created_at INTEGER NOT NULL,
|
||||
public_key TEXT NOT NULL,
|
||||
merchant_public_key TEXT NOT NULL,
|
||||
contact_data TEXT NOT NULL DEFAULT '{empty_object}',
|
||||
extra_data TEXT NOT NULL DEFAULT '{empty_object}',
|
||||
order_items TEXT NOT NULL,
|
||||
|
|
@ -88,20 +90,8 @@ async def m001_initial(db):
|
|||
invoice_id TEXT NOT NULL,
|
||||
paid BOOLEAN NOT NULL DEFAULT false,
|
||||
shipped BOOLEAN NOT NULL DEFAULT false,
|
||||
time TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
|
||||
);
|
||||
"""
|
||||
)
|
||||
|
||||
"""
|
||||
Initial market table.
|
||||
"""
|
||||
await db.execute(
|
||||
"""
|
||||
CREATE TABLE nostrmarket.markets (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
name TEXT
|
||||
time TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
|
||||
UNIQUE(event_id)
|
||||
);
|
||||
"""
|
||||
)
|
||||
|
|
@ -111,13 +101,17 @@ async def m001_initial(db):
|
|||
"""
|
||||
await db.execute(
|
||||
f"""
|
||||
CREATE TABLE nostrmarket.messages (
|
||||
CREATE TABLE nostrmarket.direct_messages (
|
||||
merchant_id TEXT NOT NULL,
|
||||
id TEXT PRIMARY KEY,
|
||||
msg TEXT NOT NULL,
|
||||
pubkey TEXT NOT NULL,
|
||||
conversation_id TEXT NOT NULL,
|
||||
timestamp TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
|
||||
);
|
||||
event_id TEXT,
|
||||
event_created_at INTEGER NOT NULL,
|
||||
message TEXT NOT NULL,
|
||||
public_key TEXT NOT NULL,
|
||||
incoming BOOLEAN NOT NULL DEFAULT false,
|
||||
time TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
|
||||
UNIQUE(event_id)
|
||||
);
|
||||
"""
|
||||
)
|
||||
|
||||
|
|
@ -126,8 +120,8 @@ async def m001_initial(db):
|
|||
Create indexes for message fetching
|
||||
"""
|
||||
await db.execute(
|
||||
"CREATE INDEX idx_messages_timestamp ON nostrmarket.messages (timestamp DESC)"
|
||||
"CREATE INDEX idx_messages_timestamp ON nostrmarket.direct_messages (time DESC)"
|
||||
)
|
||||
await db.execute(
|
||||
"CREATE INDEX idx_messages_conversations ON nostrmarket.messages (conversation_id)"
|
||||
"CREATE INDEX idx_event_id ON nostrmarket.direct_messages (event_id)"
|
||||
)
|
||||
|
|
|
|||
25
models.py
25
models.py
|
|
@ -281,7 +281,9 @@ class OrderExtra(BaseModel):
|
|||
class PartialOrder(BaseModel):
|
||||
id: str
|
||||
event_id: Optional[str]
|
||||
pubkey: str
|
||||
event_created_at: Optional[int]
|
||||
public_key: str
|
||||
merchant_public_key: str
|
||||
items: List[OrderItem]
|
||||
contact: Optional[OrderContact]
|
||||
address: Optional[str]
|
||||
|
|
@ -359,3 +361,24 @@ class PaymentRequest(BaseModel):
|
|||
id: str
|
||||
message: Optional[str]
|
||||
payment_options: List[PaymentOption]
|
||||
|
||||
|
||||
######################################## MESSAGE ########################################
|
||||
|
||||
|
||||
class PartialDirectMessage(BaseModel):
|
||||
event_id: Optional[str]
|
||||
event_created_at: Optional[int]
|
||||
message: str
|
||||
public_key: str
|
||||
incoming: bool = False
|
||||
time: Optional[int]
|
||||
|
||||
|
||||
class DirectMessage(PartialDirectMessage):
|
||||
id: str
|
||||
|
||||
@classmethod
|
||||
def from_row(cls, row: Row) -> "DirectMessage":
|
||||
dm = cls(**dict(row))
|
||||
return dm
|
||||
|
|
|
|||
|
|
@ -1,28 +1,18 @@
|
|||
from threading import Thread
|
||||
from typing import Callable
|
||||
|
||||
import httpx
|
||||
from loguru import logger
|
||||
from websocket import WebSocketApp
|
||||
|
||||
from lnbits.app import settings
|
||||
from lnbits.helpers import url_for
|
||||
|
||||
from .. import send_req_queue
|
||||
from .event import NostrEvent
|
||||
|
||||
|
||||
async def publish_nostr_event(e: NostrEvent):
|
||||
url = url_for("/nostrclient/api/v1/publish", external=True)
|
||||
data = dict(e)
|
||||
print("### published", dict(data))
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
await client.post(
|
||||
url,
|
||||
json=data,
|
||||
)
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
print("### publish_nostr_event", e.dict())
|
||||
await send_req_queue.put(["EVENT", e.dict()])
|
||||
|
||||
|
||||
async def connect_to_nostrclient_ws(
|
||||
|
|
@ -33,7 +23,7 @@ async def connect_to_nostrclient_ws(
|
|||
|
||||
logger.debug(f"Subscribing to websockets for nostrclient extension")
|
||||
ws = WebSocketApp(
|
||||
f"ws://localhost:{settings.port}/nostrclient/api/v1/filters",
|
||||
f"ws://localhost:{settings.port}/nostrclient/api/v1/relay",
|
||||
on_message=on_message,
|
||||
on_open=on_open,
|
||||
on_error=on_error,
|
||||
|
|
@ -44,23 +34,3 @@ async def connect_to_nostrclient_ws(
|
|||
wst.start()
|
||||
|
||||
return ws
|
||||
|
||||
|
||||
# async def handle_event(event, pubkeys):
|
||||
# tags = [t[1] for t in event["tags"] if t[0] == "p"]
|
||||
# to_merchant = None
|
||||
# if tags and len(tags) > 0:
|
||||
# to_merchant = tags[0]
|
||||
|
||||
# if event["pubkey"] in pubkeys or to_merchant in pubkeys:
|
||||
# logger.debug(f"Event sent to {to_merchant}")
|
||||
# pubkey = to_merchant if to_merchant in pubkeys else event["pubkey"]
|
||||
# # Send event to market extension
|
||||
# await send_event_to_market(event=event, pubkey=pubkey)
|
||||
|
||||
|
||||
# async def send_event_to_market(event: dict, pubkey: str):
|
||||
# # Sends event to market extension, for decrypt and handling
|
||||
# market_url = url_for(f"/market/api/v1/nip04/{pubkey}", external=True)
|
||||
# async with httpx.AsyncClient() as client:
|
||||
# await client.post(url=market_url, json=event)
|
||||
|
|
|
|||
223
services.py
Normal file
223
services.py
Normal file
|
|
@ -0,0 +1,223 @@
|
|||
import json
|
||||
from typing import Optional
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from lnbits.core import create_invoice, get_wallet
|
||||
|
||||
from .crud import (
|
||||
create_direct_message,
|
||||
create_order,
|
||||
get_merchant_by_pubkey,
|
||||
get_order,
|
||||
get_order_by_event_id,
|
||||
get_products_by_ids,
|
||||
get_wallet_for_product,
|
||||
update_order_paid_status,
|
||||
)
|
||||
from .helpers import order_from_json
|
||||
from .models import (
|
||||
Merchant,
|
||||
Nostrable,
|
||||
Order,
|
||||
OrderExtra,
|
||||
OrderStatusUpdate,
|
||||
PartialDirectMessage,
|
||||
PartialOrder,
|
||||
PaymentOption,
|
||||
PaymentRequest,
|
||||
)
|
||||
from .nostr.event import NostrEvent
|
||||
from .nostr.nostr_client import publish_nostr_event
|
||||
|
||||
|
||||
async def create_new_order(
|
||||
merchant_public_key: str, data: PartialOrder
|
||||
) -> Optional[PaymentRequest]:
|
||||
merchant = await get_merchant_by_pubkey(merchant_public_key)
|
||||
assert merchant, "Cannot find merchant!"
|
||||
|
||||
if await get_order(merchant.id, data.id):
|
||||
return None
|
||||
if data.event_id and await get_order_by_event_id(merchant.id, data.event_id):
|
||||
return None
|
||||
|
||||
products = await get_products_by_ids(
|
||||
merchant.id, [p.product_id for p in data.items]
|
||||
)
|
||||
data.validate_order_items(products)
|
||||
|
||||
total_amount = await data.total_sats(products)
|
||||
|
||||
wallet_id = await get_wallet_for_product(data.items[0].product_id)
|
||||
assert wallet_id, "Missing wallet for order `{data.id}`"
|
||||
|
||||
payment_hash, invoice = await create_invoice(
|
||||
wallet_id=wallet_id,
|
||||
amount=round(total_amount),
|
||||
memo=f"Order '{data.id}' for pubkey '{data.public_key}'",
|
||||
extra={
|
||||
"tag": "nostrmarket",
|
||||
"order_id": data.id,
|
||||
"merchant_pubkey": merchant.public_key,
|
||||
},
|
||||
)
|
||||
|
||||
order = Order(
|
||||
**data.dict(),
|
||||
stall_id=products[0].stall_id,
|
||||
invoice_id=payment_hash,
|
||||
total=total_amount,
|
||||
extra=await OrderExtra.from_products(products),
|
||||
)
|
||||
await create_order(merchant.id, order)
|
||||
|
||||
return PaymentRequest(
|
||||
id=data.id, payment_options=[PaymentOption(type="ln", link=invoice)]
|
||||
)
|
||||
|
||||
|
||||
async def sign_and_send_to_nostr(
|
||||
merchant: Merchant, n: Nostrable, delete=False
|
||||
) -> NostrEvent:
|
||||
event = (
|
||||
n.to_nostr_delete_event(merchant.public_key)
|
||||
if delete
|
||||
else n.to_nostr_event(merchant.public_key)
|
||||
)
|
||||
event.sig = merchant.sign_hash(bytes.fromhex(event.id))
|
||||
await publish_nostr_event(event)
|
||||
|
||||
return event
|
||||
|
||||
|
||||
async def handle_order_paid(order_id: str, merchant_pubkey: str):
|
||||
try:
|
||||
order = await update_order_paid_status(order_id, True)
|
||||
assert order, f"Paid order cannot be found. Order id: {order_id}"
|
||||
order_status = OrderStatusUpdate(
|
||||
id=order_id, message="Payment received.", paid=True, shipped=order.shipped
|
||||
)
|
||||
|
||||
merchant = await get_merchant_by_pubkey(merchant_pubkey)
|
||||
assert merchant, f"Merchant cannot be found for order {order_id}"
|
||||
dm_content = json.dumps(
|
||||
order_status.dict(), separators=(",", ":"), ensure_ascii=False
|
||||
)
|
||||
|
||||
dm_event = merchant.build_dm_event(dm_content, order.public_key)
|
||||
await publish_nostr_event(dm_event)
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
|
||||
|
||||
async def process_nostr_message(msg: str):
|
||||
try:
|
||||
type, *rest = json.loads(msg)
|
||||
if type.upper() == "EVENT":
|
||||
subscription_id, event = rest
|
||||
subscription_name, merchant_public_key = subscription_id.split(":")
|
||||
event = NostrEvent(**event)
|
||||
if event.kind == 4:
|
||||
await _handle_nip04_message(
|
||||
subscription_name, merchant_public_key, event
|
||||
)
|
||||
return
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
|
||||
|
||||
async def _handle_nip04_message(
|
||||
subscription_name: str, merchant_public_key: str, event: NostrEvent
|
||||
):
|
||||
merchant = await get_merchant_by_pubkey(merchant_public_key)
|
||||
assert merchant, f"Merchant not found for public key '{merchant_public_key}'"
|
||||
|
||||
clear_text_msg = merchant.decrypt_message(event.content, event.pubkey)
|
||||
# print("### clear_text_msg", subscription_name, clear_text_msg)
|
||||
if subscription_name == "direct-messages-in":
|
||||
await _handle_incoming_dms(event, merchant, clear_text_msg)
|
||||
else:
|
||||
await _handle_outgoing_dms(event, merchant, clear_text_msg)
|
||||
|
||||
|
||||
async def _handle_incoming_dms(
|
||||
event: NostrEvent, merchant: Merchant, clear_text_msg: str
|
||||
):
|
||||
dm_content = await _handle_dirrect_message(
|
||||
merchant.id,
|
||||
merchant.public_key,
|
||||
event.pubkey,
|
||||
event.id,
|
||||
event.created_at,
|
||||
clear_text_msg,
|
||||
)
|
||||
if dm_content:
|
||||
dm_event = merchant.build_dm_event(dm_content, event.pubkey)
|
||||
await publish_nostr_event(dm_event)
|
||||
|
||||
|
||||
async def _handle_outgoing_dms(
|
||||
event: NostrEvent, merchant: Merchant, clear_text_msg: str
|
||||
):
|
||||
sent_to = event.tag_values("p")
|
||||
if len(sent_to) != 0:
|
||||
dm = PartialDirectMessage(
|
||||
event_id=event.id,
|
||||
event_created_at=event.created_at,
|
||||
message=clear_text_msg, # exclude if json
|
||||
public_key=sent_to[0],
|
||||
incoming=True,
|
||||
)
|
||||
await create_direct_message(merchant.id, dm)
|
||||
|
||||
|
||||
async def _handle_dirrect_message(
|
||||
merchant_id: str,
|
||||
merchant_public_key: str,
|
||||
from_pubkey: str,
|
||||
event_id: str,
|
||||
event_created_at: int,
|
||||
msg: str,
|
||||
) -> Optional[str]:
|
||||
order, text_msg = order_from_json(msg)
|
||||
try:
|
||||
if order:
|
||||
order["public_key"] = from_pubkey
|
||||
order["merchant_public_key"] = merchant_public_key
|
||||
order["event_id"] = event_id
|
||||
order["event_created_at"] = event_created_at
|
||||
return await _handle_new_order(PartialOrder(**order))
|
||||
else:
|
||||
# print("### text_msg", text_msg, event_created_at, event_id)
|
||||
dm = PartialDirectMessage(
|
||||
event_id=event_id,
|
||||
event_created_at=event_created_at,
|
||||
message=text_msg,
|
||||
public_key=from_pubkey,
|
||||
incoming=True,
|
||||
)
|
||||
await create_direct_message(merchant_id, dm)
|
||||
return None
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
return None
|
||||
|
||||
|
||||
async def _handle_new_order(order: PartialOrder) -> Optional[str]:
|
||||
### todo: check that event_id not parsed already
|
||||
|
||||
order.validate_order()
|
||||
|
||||
first_product_id = order.items[0].product_id
|
||||
wallet_id = await get_wallet_for_product(first_product_id)
|
||||
assert wallet_id, f"Cannot find wallet id for product id: {first_product_id}"
|
||||
|
||||
wallet = await get_wallet(wallet_id)
|
||||
assert wallet, f"Cannot find wallet for product id: {first_product_id}"
|
||||
|
||||
new_order = await create_new_order(wallet.user, order)
|
||||
if new_order:
|
||||
return json.dumps(new_order.dict(), separators=(",", ":"), ensure_ascii=False)
|
||||
|
||||
return None
|
||||
|
|
@ -345,8 +345,9 @@ async function customerStall(path) {
|
|||
let json = JSON.parse(text)
|
||||
if (json.id != this.activeOrder) return
|
||||
if (json.payment_options) {
|
||||
let payment_request = json.payment_options.find(o => o.type == 'ln')
|
||||
.link
|
||||
let payment_request = json.payment_options.find(
|
||||
o => o.type == 'ln'
|
||||
).link
|
||||
if (!payment_request) return
|
||||
this.loading = false
|
||||
this.qrCodeDialog.data.payment_request = payment_request
|
||||
|
|
|
|||
59
static/components/direct-messages/direct-messages.html
Normal file
59
static/components/direct-messages/direct-messages.html
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
<div>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<h6 class="text-subtitle1 q-my-none">Messages</h6>
|
||||
</q-card-section>
|
||||
<q-card-section class="q-pa-none">
|
||||
<q-separator></q-separator>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<!-- <q-select
|
||||
v-model="customerKey"
|
||||
:options="Object.keys(messages).map(k => ({label: `${k.slice(0, 25)}...`, value: k}))"
|
||||
label="Customers"
|
||||
@input="chatRoom(customerKey)"
|
||||
emit-value
|
||||
></q-select> -->
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<div class="chat-container" ref="chatCard">
|
||||
<div class="chat-box">
|
||||
<div class="chat-messages" style="height: 45vh">
|
||||
<q-chat-message
|
||||
v-for="(dm, index) in messages"
|
||||
:key="index"
|
||||
:name="dm.incoming ? 'customer': 'me'"
|
||||
:text="[dm.message]"
|
||||
:sent="!dm.incoming"
|
||||
:bg-color="dm.incoming ? 'white' : 'light-green-2'"
|
||||
:class="'chat-mesage-index-'+index"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<q-card-section>
|
||||
<q-form @submit="sendDirectMesage" class="full-width chat-input">
|
||||
<q-input
|
||||
ref="newMessage"
|
||||
v-model="newMessage"
|
||||
placeholder="Message"
|
||||
class="full-width"
|
||||
dense
|
||||
outlined
|
||||
>
|
||||
<template>
|
||||
<q-btn
|
||||
round
|
||||
dense
|
||||
flat
|
||||
type="submit"
|
||||
icon="send"
|
||||
color="primary"
|
||||
/>
|
||||
</template>
|
||||
</q-input>
|
||||
</q-form>
|
||||
</q-card-section>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
72
static/components/direct-messages/direct-messages.js
Normal file
72
static/components/direct-messages/direct-messages.js
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
async function directMessages(path) {
|
||||
const template = await loadTemplateAsync(path)
|
||||
Vue.component('direct-messages', {
|
||||
name: 'direct-messages',
|
||||
props: ['adminkey', 'inkey'],
|
||||
template,
|
||||
|
||||
data: function () {
|
||||
return {
|
||||
activePublicKey:
|
||||
'83d07a79496f4cbdc50ca585741a79a2df1fd938cfa449f0fccb0ab7352115dd',
|
||||
messages: [],
|
||||
newMessage: ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
sendMessage: async function () {},
|
||||
getDirectMessages: async function () {
|
||||
if (!this.activePublicKey) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
'GET',
|
||||
'/nostrmarket/api/v1/message/' + this.activePublicKey,
|
||||
this.inkey
|
||||
)
|
||||
this.messages = data
|
||||
console.log(
|
||||
'### this.messages',
|
||||
this.messages.map(m => m.message)
|
||||
)
|
||||
this.focusOnChatBox(this.messages.length - 1)
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
},
|
||||
sendDirectMesage: async function () {
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
'POST',
|
||||
'/nostrmarket/api/v1/message',
|
||||
this.adminkey,
|
||||
{
|
||||
message: this.newMessage,
|
||||
public_key: this.activePublicKey
|
||||
}
|
||||
)
|
||||
this.messages = this.messages.concat([data])
|
||||
console.log('### this.messages', this.messages)
|
||||
this.newMessage = ''
|
||||
this.focusOnChatBox(this.messages.length - 1)
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
},
|
||||
focusOnChatBox: function (index) {
|
||||
setTimeout(() => {
|
||||
const lastChatBox = document.getElementsByClassName(
|
||||
`chat-mesage-index-${index}`
|
||||
)
|
||||
if (lastChatBox && lastChatBox[0]) {
|
||||
lastChatBox[0].scrollIntoView()
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
},
|
||||
created: async function () {
|
||||
await this.getDirectMessages()
|
||||
}
|
||||
})
|
||||
}
|
||||
37
static/components/merchant-details/merchant-details.html
Normal file
37
static/components/merchant-details/merchant-details.html
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
<div>
|
||||
<q-btn-dropdown
|
||||
split
|
||||
unelevated
|
||||
color="primary"
|
||||
icon="storefront"
|
||||
label="Merchant"
|
||||
>
|
||||
<q-list>
|
||||
<q-item disable clickable v-close-popup>
|
||||
<q-item-section>
|
||||
<q-item-label>Merchant Profile</q-item-label>
|
||||
<q-item-label caption
|
||||
>Edit the merchand name, description, etc</q-item-label
|
||||
>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item @click="toggleMerchantKeys" clickable v-close-popup>
|
||||
<q-item-section>
|
||||
<q-item-label v-if="!showKeys">Show Keys</q-item-label>
|
||||
<q-item-label v-else>Hide Keys</q-item-label>
|
||||
<q-item-label caption
|
||||
>Show merchant public and private keys</q-item-label
|
||||
>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item @click="deleteMerchant" clickable v-close-popup>
|
||||
<q-item-section>
|
||||
<q-item-label>Delete Merchant</q-item-label>
|
||||
<q-item-label caption
|
||||
>Delete all stalls, products and orders</q-item-label
|
||||
>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list></q-btn-dropdown
|
||||
>
|
||||
</div>
|
||||
48
static/components/merchant-details/merchant-details.js
Normal file
48
static/components/merchant-details/merchant-details.js
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
async function merchantDetails(path) {
|
||||
const template = await loadTemplateAsync(path)
|
||||
Vue.component('merchant-details', {
|
||||
name: 'merchant-details',
|
||||
props: ['merchant-id', 'adminkey', 'inkey'],
|
||||
template,
|
||||
|
||||
data: function () {
|
||||
return {
|
||||
showKeys: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleMerchantKeys: async function () {
|
||||
this.showKeys = !this.showKeys
|
||||
this.$emit('show-keys', this.showKeys)
|
||||
},
|
||||
deleteMerchant: function () {
|
||||
LNbits.utils
|
||||
.confirmDialog(
|
||||
`
|
||||
Stalls, products and orders will be deleted also!
|
||||
Are you sure you want to delete this merchant?
|
||||
`
|
||||
)
|
||||
.onOk(async () => {
|
||||
try {
|
||||
await LNbits.api.request(
|
||||
'DELETE',
|
||||
'/nostrmarket/api/v1/merchant/' + this.merchantId,
|
||||
this.adminkey
|
||||
)
|
||||
this.$emit('merchant-deleted', this.merchantId)
|
||||
this.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Merchant Deleted',
|
||||
timeout: 5000
|
||||
})
|
||||
} catch (error) {
|
||||
console.warn(error)
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
created: async function () {}
|
||||
})
|
||||
}
|
||||
|
|
@ -43,8 +43,8 @@
|
|||
></q-checkbox>
|
||||
</q-td>
|
||||
|
||||
<q-td key="pubkey" :props="props">
|
||||
{{toShortId(props.row.pubkey)}}
|
||||
<q-td key="public_key" :props="props">
|
||||
{{toShortId(props.row.public_key)}}
|
||||
</q-td>
|
||||
<q-td key="time" :props="props"> {{formatDate(props.row.time)}} </q-td>
|
||||
</q-tr>
|
||||
|
|
@ -115,7 +115,7 @@
|
|||
dense
|
||||
readonly
|
||||
disabled
|
||||
v-model.trim="props.row.pubkey"
|
||||
v-model.trim="props.row.public_key"
|
||||
type="text"
|
||||
></q-input>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
unelevated
|
||||
color="primary"
|
||||
icon="public"
|
||||
label="Zones"
|
||||
@click="openZoneDialog()"
|
||||
>
|
||||
<q-list>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
unelevated
|
||||
color="green"
|
||||
class="float-left"
|
||||
>New Stall</q-btn
|
||||
>New Stall (Store)</q-btn
|
||||
>
|
||||
<q-input
|
||||
borderless
|
||||
|
|
|
|||
|
|
@ -6,6 +6,10 @@ const merchant = async () => {
|
|||
await stallDetails('static/components/stall-details/stall-details.html')
|
||||
await stallList('static/components/stall-list/stall-list.html')
|
||||
await orderList('static/components/order-list/order-list.html')
|
||||
await directMessages('static/components/direct-messages/direct-messages.html')
|
||||
await merchantDetails(
|
||||
'static/components/merchant-details/merchant-details.html'
|
||||
)
|
||||
|
||||
const nostr = window.NostrTools
|
||||
|
||||
|
|
@ -51,6 +55,9 @@ const merchant = async () => {
|
|||
showImportKeysDialog: async function () {
|
||||
this.importKeyDialog.show = true
|
||||
},
|
||||
toggleMerchantKeys: function (value) {
|
||||
this.showKeys = value
|
||||
},
|
||||
createMerchant: async function (privateKey) {
|
||||
try {
|
||||
const pubkey = nostr.getPublicKey(privateKey)
|
||||
|
|
|
|||
127
tasks.py
127
tasks.py
|
|
@ -2,26 +2,20 @@ import asyncio
|
|||
import json
|
||||
from asyncio import Queue
|
||||
|
||||
import httpx
|
||||
import websocket
|
||||
from loguru import logger
|
||||
from websocket import WebSocketApp
|
||||
|
||||
from lnbits.core import get_wallet
|
||||
from lnbits.core.models import Payment
|
||||
from lnbits.helpers import Optional, url_for
|
||||
from lnbits.tasks import register_invoice_listener
|
||||
|
||||
from .crud import (
|
||||
get_merchant_by_pubkey,
|
||||
get_last_direct_messages_time,
|
||||
get_last_order_time,
|
||||
get_public_keys_for_merchants,
|
||||
get_wallet_for_product,
|
||||
update_order_paid_status,
|
||||
)
|
||||
from .helpers import order_from_json
|
||||
from .models import OrderStatusUpdate, PartialOrder
|
||||
from .nostr.event import NostrEvent
|
||||
from .nostr.nostr_client import connect_to_nostrclient_ws, publish_nostr_event
|
||||
from .nostr.nostr_client import connect_to_nostrclient_ws
|
||||
from .services import handle_order_paid, process_nostr_message
|
||||
|
||||
|
||||
async def wait_for_paid_invoices():
|
||||
|
|
@ -45,38 +39,14 @@ async def on_invoice_paid(payment: Payment) -> None:
|
|||
await handle_order_paid(order_id, merchant_pubkey)
|
||||
|
||||
|
||||
async def handle_order_paid(order_id: str, merchant_pubkey: str):
|
||||
try:
|
||||
order = await update_order_paid_status(order_id, True)
|
||||
assert order, f"Paid order cannot be found. Order id: {order_id}"
|
||||
order_status = OrderStatusUpdate(
|
||||
id=order_id, message="Payment received.", paid=True, shipped=order.shipped
|
||||
)
|
||||
|
||||
merchant = await get_merchant_by_pubkey(merchant_pubkey)
|
||||
assert merchant, f"Merchant cannot be found for order {order_id}"
|
||||
dm_content = json.dumps(
|
||||
order_status.dict(), separators=(",", ":"), ensure_ascii=False
|
||||
)
|
||||
|
||||
dm_event = merchant.build_dm_event(dm_content, order.pubkey)
|
||||
await publish_nostr_event(dm_event)
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
|
||||
|
||||
async def subscribe_to_nostr_client(recieve_event_queue: Queue, send_req_queue: Queue):
|
||||
print("### subscribe_nostrclient_ws")
|
||||
|
||||
def on_open(_):
|
||||
logger.info("Connected to 'nostrclient' websocket")
|
||||
|
||||
def on_message(_, message):
|
||||
print("### on_message", message)
|
||||
# print("### on_message", message)
|
||||
recieve_event_queue.put_nowait(message)
|
||||
|
||||
# wait for 'nostrclient' extension to initialize
|
||||
await asyncio.sleep(5)
|
||||
ws: WebSocketApp = None
|
||||
while True:
|
||||
try:
|
||||
|
|
@ -98,80 +68,23 @@ async def subscribe_to_nostr_client(recieve_event_queue: Queue, send_req_queue:
|
|||
async def wait_for_nostr_events(recieve_event_queue: Queue, send_req_queue: Queue):
|
||||
public_keys = await get_public_keys_for_merchants()
|
||||
for p in public_keys:
|
||||
last_order_time = await get_last_order_time(p)
|
||||
last_dm_time = await get_last_direct_messages_time(p)
|
||||
since = max(last_order_time, last_dm_time)
|
||||
|
||||
in_messages_filter = {"kind": 4, "#p": [p]}
|
||||
out_messages_filter = {"kind": 4, "authors": [p]}
|
||||
if since != 0:
|
||||
in_messages_filter["since"] = since
|
||||
# out_messages_filter["since"] = since
|
||||
print("### in_messages_filter", in_messages_filter)
|
||||
print("### out_messages_filter", out_messages_filter)
|
||||
|
||||
await send_req_queue.put(["REQ", f"direct-messages-in:{p}", in_messages_filter])
|
||||
await send_req_queue.put(
|
||||
["REQ", f"direct-messages:{p}", {"kind": 4, "#p": [p]}]
|
||||
["REQ", f"direct-messages-out:{p}", out_messages_filter]
|
||||
)
|
||||
|
||||
while True:
|
||||
message = await recieve_event_queue.get()
|
||||
await handle_message(message)
|
||||
|
||||
|
||||
async def handle_message(msg: str):
|
||||
try:
|
||||
type, subscription_id, event = json.loads(msg)
|
||||
_, public_key = subscription_id.split(":")
|
||||
if type.upper() == "EVENT":
|
||||
event = NostrEvent(**event)
|
||||
if event.kind == 4:
|
||||
await handle_nip04_message(public_key, event)
|
||||
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
|
||||
|
||||
async def handle_nip04_message(public_key: str, event: NostrEvent):
|
||||
merchant = await get_merchant_by_pubkey(public_key)
|
||||
assert merchant, f"Merchant not found for public key '{public_key}'"
|
||||
|
||||
clear_text_msg = merchant.decrypt_message(event.content, event.pubkey)
|
||||
dm_content = await handle_dirrect_message(event.pubkey, event.id, clear_text_msg)
|
||||
if dm_content:
|
||||
dm_event = merchant.build_dm_event(dm_content, event.pubkey)
|
||||
await publish_nostr_event(dm_event)
|
||||
|
||||
|
||||
async def handle_dirrect_message(
|
||||
from_pubkey: str, event_id: str, msg: str
|
||||
) -> Optional[str]:
|
||||
order, text_msg = order_from_json(msg)
|
||||
try:
|
||||
if order:
|
||||
order["pubkey"] = from_pubkey
|
||||
order["event_id"] = event_id
|
||||
return await handle_new_order(PartialOrder(**order))
|
||||
else:
|
||||
print("### text_msg", text_msg)
|
||||
return None
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
return None
|
||||
|
||||
|
||||
async def handle_new_order(order: PartialOrder):
|
||||
### todo: check that event_id not parsed already
|
||||
|
||||
order.validate_order()
|
||||
|
||||
first_product_id = order.items[0].product_id
|
||||
wallet_id = await get_wallet_for_product(first_product_id)
|
||||
assert wallet_id, f"Cannot find wallet id for product id: {first_product_id}"
|
||||
|
||||
wallet = await get_wallet(wallet_id)
|
||||
assert wallet, f"Cannot find wallet for product id: {first_product_id}"
|
||||
|
||||
market_url = url_for(f"/nostrmarket/api/v1/order", external=True)
|
||||
async with httpx.AsyncClient() as client:
|
||||
resp = await client.post(
|
||||
url=market_url,
|
||||
headers={
|
||||
"X-Api-Key": wallet.adminkey,
|
||||
},
|
||||
json=order.dict(),
|
||||
)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
if data:
|
||||
return json.dumps(data, separators=(",", ":"), ensure_ascii=False)
|
||||
|
||||
return None
|
||||
await process_nostr_message(message)
|
||||
|
|
|
|||
|
|
@ -27,8 +27,6 @@
|
|||
></small
|
||||
>
|
||||
</p>
|
||||
<br />
|
||||
<br />
|
||||
<a
|
||||
class="text-secondary"
|
||||
target="_blank"
|
||||
|
|
|
|||
|
|
@ -2,7 +2,46 @@
|
|||
%} {% block page %}
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-md-7 q-gutter-y-md">
|
||||
<q-card v-if="!merchant">
|
||||
<div v-if="merchant && merchant.id">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<div class="row">
|
||||
<div class="col-2">
|
||||
<merchant-details
|
||||
:merchant-id="merchant.id"
|
||||
:inkey="g.user.wallets[0].inkey"
|
||||
:adminkey="g.user.wallets[0].adminkey"
|
||||
@show-keys="toggleMerchantKeys"
|
||||
></merchant-details>
|
||||
</div>
|
||||
<div class="col-8"></div>
|
||||
<div class="col-2">
|
||||
<shipping-zones
|
||||
:inkey="g.user.wallets[0].inkey"
|
||||
:adminkey="g.user.wallets[0].adminkey"
|
||||
class="float-right"
|
||||
></shipping-zones>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-card-section v-if="showKeys">
|
||||
<key-pair
|
||||
:public-key="merchant.public_key"
|
||||
:private-key="merchant.private_key"
|
||||
></key-pair>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
<q-card class="q-mt-lg">
|
||||
<q-card-section>
|
||||
<stall-list
|
||||
:adminkey="g.user.wallets[0].adminkey"
|
||||
:inkey="g.user.wallets[0].inkey"
|
||||
:wallet-options="g.user.walletOptions"
|
||||
></stall-list>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
<q-card v-else>
|
||||
<q-card-section>
|
||||
<span class="text-h4">Wellcome to Nostr Market!</span><br />
|
||||
In Nostr Market, merchant and customer communicate via NOSTR relays, so
|
||||
|
|
@ -57,62 +96,31 @@
|
|||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
<div v-else>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<div class="row">
|
||||
<div class="col-8"></div>
|
||||
<div class="col-2">
|
||||
<shipping-zones
|
||||
:inkey="g.user.wallets[0].inkey"
|
||||
:adminkey="g.user.wallets[0].adminkey"
|
||||
class="float-right"
|
||||
></shipping-zones>
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<q-btn
|
||||
@click="showKeys = !showKeys"
|
||||
icon="vpn_key"
|
||||
color="primary"
|
||||
class="float-right"
|
||||
>
|
||||
<q-tooltip> Show Public and Private keys </q-tooltip>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-card-section v-if="showKeys">
|
||||
<key-pair
|
||||
:public-key="merchant.public_key"
|
||||
:private-key="merchant.private_key"
|
||||
></key-pair>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
<q-card class="q-mt-lg">
|
||||
<q-card-section>
|
||||
<stall-list
|
||||
:adminkey="g.user.wallets[0].adminkey"
|
||||
:inkey="g.user.wallets[0].inkey"
|
||||
:wallet-options="g.user.walletOptions"
|
||||
></stall-list>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-5 q-gutter-y-md">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<h6 class="text-subtitle1 q-my-none">
|
||||
{{SITE_TITLE}} Nostr Market Extension
|
||||
</h6>
|
||||
</q-card-section>
|
||||
<q-card-section class="q-pa-none">
|
||||
<q-separator></q-separator>
|
||||
<q-list> {% include "nostrmarket/_api_docs.html" %} </q-list>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
<div class="col-12">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<h6 class="text-subtitle1 q-my-none">
|
||||
{{SITE_TITLE}} Nostr Market Extension
|
||||
</h6>
|
||||
</q-card-section>
|
||||
<q-card-section class="q-pa-none">
|
||||
<q-separator></q-separator>
|
||||
<q-list> {% include "nostrmarket/_api_docs.html" %} </q-list>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
<div v-if="merchant && merchant.id" class="col-12">
|
||||
<direct-messages
|
||||
:inkey="g.user.wallets[0].inkey"
|
||||
:adminkey="g.user.wallets[0].adminkey"
|
||||
>
|
||||
</direct-messages>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<q-dialog v-model="importKeyDialog.show" position="top">
|
||||
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
|
||||
|
|
@ -141,6 +149,23 @@
|
|||
</div>
|
||||
</div>
|
||||
{% endblock%}{% block scripts %} {{ window_vars(user) }}
|
||||
|
||||
<style scoped>
|
||||
.chat-container {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-rows: 1fr auto;
|
||||
height: 50vh;
|
||||
}
|
||||
|
||||
.chat-box {
|
||||
padding: 1rem;
|
||||
overflow-y: auto;
|
||||
margin-left: auto;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- todo: serve locally -->
|
||||
<script src="https://unpkg.com/nostr-tools/lib/nostr.bundle.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/pica@6.1.1/dist/pica.min.js"></script>
|
||||
|
|
@ -151,6 +176,8 @@
|
|||
<script src="{{ url_for('nostrmarket_static', path='components/stall-details/stall-details.js') }}"></script>
|
||||
<script src="{{ url_for('nostrmarket_static', path='components/stall-list/stall-list.js') }}"></script>
|
||||
<script src="{{ url_for('nostrmarket_static', path='components/order-list/order-list.js') }}"></script>
|
||||
<script src="{{ url_for('nostrmarket_static', path='components/direct-messages/direct-messages.js') }}"></script>
|
||||
<script src="{{ url_for('nostrmarket_static', path='components/merchant-details/merchant-details.js') }}"></script>
|
||||
<script src="{{ url_for('nostrmarket_static', path='js/index.js') }}"></script>
|
||||
|
||||
{% endblock %}
|
||||
|
|
|
|||
285
views_api.py
285
views_api.py
|
|
@ -6,7 +6,6 @@ from fastapi import Depends
|
|||
from fastapi.exceptions import HTTPException
|
||||
from loguru import logger
|
||||
|
||||
from lnbits.core import create_invoice
|
||||
from lnbits.decorators import (
|
||||
WalletTypeInfo,
|
||||
check_admin,
|
||||
|
|
@ -18,25 +17,29 @@ from lnbits.utils.exchange_rates import currencies
|
|||
|
||||
from . import nostrmarket_ext, scheduled_tasks
|
||||
from .crud import (
|
||||
create_direct_message,
|
||||
create_merchant,
|
||||
create_order,
|
||||
create_product,
|
||||
create_stall,
|
||||
create_zone,
|
||||
delete_merchant_direct_messages,
|
||||
delete_merchant_orders,
|
||||
delete_merchant_products,
|
||||
delete_merchant_stalls,
|
||||
delete_merchant_zones,
|
||||
delete_merchants,
|
||||
delete_product,
|
||||
delete_stall,
|
||||
delete_zone,
|
||||
get_direct_messages,
|
||||
get_merchant_for_user,
|
||||
get_order,
|
||||
get_order_by_event_id,
|
||||
get_orders,
|
||||
get_orders_for_stall,
|
||||
get_product,
|
||||
get_products,
|
||||
get_products_by_ids,
|
||||
get_stall,
|
||||
get_stalls,
|
||||
get_wallet_for_product,
|
||||
get_zone,
|
||||
get_zones,
|
||||
update_order_shipped_status,
|
||||
|
|
@ -45,24 +48,21 @@ from .crud import (
|
|||
update_zone,
|
||||
)
|
||||
from .models import (
|
||||
DirectMessage,
|
||||
Merchant,
|
||||
Nostrable,
|
||||
Order,
|
||||
OrderExtra,
|
||||
OrderStatusUpdate,
|
||||
PartialDirectMessage,
|
||||
PartialMerchant,
|
||||
PartialOrder,
|
||||
PartialProduct,
|
||||
PartialStall,
|
||||
PartialZone,
|
||||
PaymentOption,
|
||||
PaymentRequest,
|
||||
Product,
|
||||
Stall,
|
||||
Zone,
|
||||
)
|
||||
from .nostr.event import NostrEvent
|
||||
from .nostr.nostr_client import publish_nostr_event
|
||||
from .services import sign_and_send_to_nostr
|
||||
|
||||
######################################## MERCHANT ########################################
|
||||
|
||||
|
|
@ -75,6 +75,7 @@ async def api_create_merchant(
|
|||
|
||||
try:
|
||||
merchant = await create_merchant(wallet.wallet.user, data)
|
||||
|
||||
return merchant
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
|
|
@ -100,13 +101,40 @@ async def api_get_merchant(
|
|||
)
|
||||
|
||||
|
||||
@nostrmarket_ext.delete("/api/v1/merchant/{merchant_id}")
|
||||
async def api_delete_merchant(
|
||||
merchant_id: str,
|
||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||
):
|
||||
|
||||
try:
|
||||
merchant = await get_merchant_for_user(wallet.wallet.user)
|
||||
assert merchant, "Merchant cannot be found"
|
||||
assert merchant.id == merchant_id, "Wrong merchant ID"
|
||||
|
||||
await delete_merchant_orders(merchant.id)
|
||||
await delete_merchant_products(merchant.id)
|
||||
await delete_merchant_stalls(merchant.id)
|
||||
await delete_merchant_direct_messages(merchant.id)
|
||||
await delete_merchant_zones(merchant.id)
|
||||
await delete_merchants(merchant.id)
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail="Cannot get merchant",
|
||||
)
|
||||
|
||||
|
||||
######################################## ZONES ########################################
|
||||
|
||||
|
||||
@nostrmarket_ext.get("/api/v1/zone")
|
||||
async def api_get_zones(wallet: WalletTypeInfo = Depends(get_key_type)) -> List[Zone]:
|
||||
try:
|
||||
return await get_zones(wallet.wallet.user)
|
||||
merchant = await get_merchant_for_user(wallet.wallet.user)
|
||||
assert merchant, "Merchant cannot be found"
|
||||
return await get_zones(merchant.id)
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
raise HTTPException(
|
||||
|
|
@ -120,7 +148,9 @@ async def api_create_zone(
|
|||
data: PartialZone, wallet: WalletTypeInfo = Depends(require_admin_key)
|
||||
):
|
||||
try:
|
||||
zone = await create_zone(wallet.wallet.user, data)
|
||||
merchant = await get_merchant_for_user(wallet.wallet.user)
|
||||
assert merchant, "Merchant cannot be found"
|
||||
zone = await create_zone(merchant.id, data)
|
||||
return zone
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
|
|
@ -137,7 +167,9 @@ async def api_update_zone(
|
|||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||
) -> Zone:
|
||||
try:
|
||||
zone = await get_zone(wallet.wallet.user, zone_id)
|
||||
merchant = await get_merchant_for_user(wallet.wallet.user)
|
||||
assert merchant, "Merchant cannot be found"
|
||||
zone = await get_zone(merchant.id, zone_id)
|
||||
if not zone:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND,
|
||||
|
|
@ -159,7 +191,9 @@ async def api_update_zone(
|
|||
@nostrmarket_ext.delete("/api/v1/zone/{zone_id}")
|
||||
async def api_delete_zone(zone_id, wallet: WalletTypeInfo = Depends(require_admin_key)):
|
||||
try:
|
||||
zone = await get_zone(wallet.wallet.user, zone_id)
|
||||
merchant = await get_merchant_for_user(wallet.wallet.user)
|
||||
assert merchant, "Merchant cannot be found"
|
||||
zone = await get_zone(merchant.id, zone_id)
|
||||
|
||||
if not zone:
|
||||
raise HTTPException(
|
||||
|
|
@ -167,7 +201,7 @@ async def api_delete_zone(zone_id, wallet: WalletTypeInfo = Depends(require_admi
|
|||
detail="Zone does not exist.",
|
||||
)
|
||||
|
||||
await delete_zone(zone_id)
|
||||
await delete_zone(wallet.wallet.user, zone_id)
|
||||
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
|
|
@ -188,12 +222,14 @@ async def api_create_stall(
|
|||
try:
|
||||
data.validate_stall()
|
||||
|
||||
stall = await create_stall(wallet.wallet.user, data=data)
|
||||
merchant = await get_merchant_for_user(wallet.wallet.user)
|
||||
assert merchant, "Merchant cannot be found"
|
||||
stall = await create_stall(merchant.id, data=data)
|
||||
|
||||
event = await sign_and_send_to_nostr(wallet.wallet.user, stall)
|
||||
event = await sign_and_send_to_nostr(merchant, stall)
|
||||
|
||||
stall.config.event_id = event.id
|
||||
await update_stall(wallet.wallet.user, stall)
|
||||
await update_stall(merchant.id, stall)
|
||||
|
||||
return stall
|
||||
except ValueError as ex:
|
||||
|
|
@ -217,13 +253,16 @@ async def api_update_stall(
|
|||
try:
|
||||
data.validate_stall()
|
||||
|
||||
stall = await update_stall(wallet.wallet.user, data)
|
||||
merchant = await get_merchant_for_user(wallet.wallet.user)
|
||||
assert merchant, "Merchant cannot be found"
|
||||
|
||||
stall = await update_stall(merchant.id, data)
|
||||
assert stall, "Cannot update stall"
|
||||
|
||||
event = await sign_and_send_to_nostr(wallet.wallet.user, stall)
|
||||
event = await sign_and_send_to_nostr(merchant, stall)
|
||||
|
||||
stall.config.event_id = event.id
|
||||
await update_stall(wallet.wallet.user, stall)
|
||||
await update_stall(merchant.id, stall)
|
||||
|
||||
return stall
|
||||
except HTTPException as ex:
|
||||
|
|
@ -244,7 +283,9 @@ async def api_update_stall(
|
|||
@nostrmarket_ext.get("/api/v1/stall/{stall_id}")
|
||||
async def api_get_stall(stall_id: str, wallet: WalletTypeInfo = Depends(get_key_type)):
|
||||
try:
|
||||
stall = await get_stall(wallet.wallet.user, stall_id)
|
||||
merchant = await get_merchant_for_user(wallet.wallet.user)
|
||||
assert merchant, "Merchant cannot be found"
|
||||
stall = await get_stall(merchant.id, stall_id)
|
||||
if not stall:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND,
|
||||
|
|
@ -264,7 +305,9 @@ async def api_get_stall(stall_id: str, wallet: WalletTypeInfo = Depends(get_key_
|
|||
@nostrmarket_ext.get("/api/v1/stall")
|
||||
async def api_get_stalls(wallet: WalletTypeInfo = Depends(get_key_type)):
|
||||
try:
|
||||
stalls = await get_stalls(wallet.wallet.user)
|
||||
merchant = await get_merchant_for_user(wallet.wallet.user)
|
||||
assert merchant, "Merchant cannot be found"
|
||||
stalls = await get_stalls(merchant.id)
|
||||
return stalls
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
|
|
@ -280,7 +323,9 @@ async def api_get_stall_products(
|
|||
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
||||
):
|
||||
try:
|
||||
products = await get_products(wallet.wallet.user, stall_id)
|
||||
merchant = await get_merchant_for_user(wallet.wallet.user)
|
||||
assert merchant, "Merchant cannot be found"
|
||||
products = await get_products(merchant.id, stall_id)
|
||||
return products
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
|
|
@ -296,7 +341,9 @@ async def api_get_stall_orders(
|
|||
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
||||
):
|
||||
try:
|
||||
orders = await get_orders_for_stall(wallet.wallet.user, stall_id)
|
||||
merchant = await get_merchant_for_user(wallet.wallet.user)
|
||||
assert merchant, "Merchant cannot be found"
|
||||
orders = await get_orders_for_stall(merchant.id, stall_id)
|
||||
return orders
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
|
|
@ -311,19 +358,21 @@ async def api_delete_stall(
|
|||
stall_id: str, wallet: WalletTypeInfo = Depends(require_admin_key)
|
||||
):
|
||||
try:
|
||||
stall = await get_stall(wallet.wallet.user, stall_id)
|
||||
merchant = await get_merchant_for_user(wallet.wallet.user)
|
||||
assert merchant, "Merchant cannot be found"
|
||||
stall = await get_stall(merchant.id, stall_id)
|
||||
if not stall:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND,
|
||||
detail="Stall does not exist.",
|
||||
)
|
||||
|
||||
await delete_stall(wallet.wallet.user, stall_id)
|
||||
await delete_stall(merchant.id, stall_id)
|
||||
|
||||
event = await sign_and_send_to_nostr(wallet.wallet.user, stall, True)
|
||||
event = await sign_and_send_to_nostr(merchant, stall, True)
|
||||
|
||||
stall.config.event_id = event.id
|
||||
await update_stall(wallet.wallet.user, stall)
|
||||
await update_stall(merchant.id, stall)
|
||||
|
||||
except HTTPException as ex:
|
||||
raise ex
|
||||
|
|
@ -345,17 +394,19 @@ async def api_create_product(
|
|||
) -> Product:
|
||||
try:
|
||||
data.validate_product()
|
||||
merchant = await get_merchant_for_user(wallet.wallet.user)
|
||||
assert merchant, "Merchant cannot be found"
|
||||
|
||||
stall = await get_stall(wallet.wallet.user, data.stall_id)
|
||||
stall = await get_stall(merchant.id, data.stall_id)
|
||||
assert stall, "Stall missing for product"
|
||||
data.config.currency = stall.currency
|
||||
|
||||
product = await create_product(wallet.wallet.user, data=data)
|
||||
product = await create_product(merchant.id, data=data)
|
||||
|
||||
event = await sign_and_send_to_nostr(wallet.wallet.user, product)
|
||||
event = await sign_and_send_to_nostr(merchant, product)
|
||||
|
||||
product.config.event_id = event.id
|
||||
await update_product(wallet.wallet.user, product)
|
||||
await update_product(merchant.id, product)
|
||||
|
||||
return product
|
||||
except ValueError as ex:
|
||||
|
|
@ -382,17 +433,19 @@ async def api_update_product(
|
|||
raise ValueError("Bad product ID")
|
||||
|
||||
product.validate_product()
|
||||
merchant = await get_merchant_for_user(wallet.wallet.user)
|
||||
assert merchant, "Merchant cannot be found"
|
||||
|
||||
stall = await get_stall(wallet.wallet.user, product.stall_id)
|
||||
stall = await get_stall(merchant.id, product.stall_id)
|
||||
assert stall, "Stall missing for product"
|
||||
product.config.currency = stall.currency
|
||||
|
||||
product = await update_product(wallet.wallet.user, product)
|
||||
product = await update_product(merchant.id, product)
|
||||
|
||||
event = await sign_and_send_to_nostr(wallet.wallet.user, product)
|
||||
event = await sign_and_send_to_nostr(merchant, product)
|
||||
|
||||
product.config.event_id = event.id
|
||||
await update_product(wallet.wallet.user, product)
|
||||
await update_product(merchant.id, product)
|
||||
|
||||
return product
|
||||
except ValueError as ex:
|
||||
|
|
@ -414,7 +467,10 @@ async def api_get_product(
|
|||
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
||||
) -> Optional[Product]:
|
||||
try:
|
||||
products = await get_product(wallet.wallet.user, product_id)
|
||||
merchant = await get_merchant_for_user(wallet.wallet.user)
|
||||
assert merchant, "Merchant cannot be found"
|
||||
|
||||
products = await get_product(merchant.id, product_id)
|
||||
return products
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
|
|
@ -430,15 +486,18 @@ async def api_delete_product(
|
|||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||
):
|
||||
try:
|
||||
product = await get_product(wallet.wallet.user, product_id)
|
||||
merchant = await get_merchant_for_user(wallet.wallet.user)
|
||||
assert merchant, "Merchant cannot be found"
|
||||
|
||||
product = await get_product(merchant.id, product_id)
|
||||
if not product:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND,
|
||||
detail="Product does not exist.",
|
||||
)
|
||||
|
||||
await delete_product(wallet.wallet.user, product_id)
|
||||
await sign_and_send_to_nostr(wallet.wallet.user, product, True)
|
||||
await delete_product(merchant.id, product_id)
|
||||
await sign_and_send_to_nostr(merchant, product, True)
|
||||
|
||||
except HTTPException as ex:
|
||||
raise ex
|
||||
|
|
@ -453,69 +512,15 @@ async def api_delete_product(
|
|||
######################################## ORDERS ########################################
|
||||
|
||||
|
||||
@nostrmarket_ext.post("/api/v1/order")
|
||||
async def api_create_order(
|
||||
data: PartialOrder, wallet: WalletTypeInfo = Depends(require_admin_key)
|
||||
) -> Optional[PaymentRequest]:
|
||||
try:
|
||||
# print("### new order: ", json.dumps(data.dict()))
|
||||
if await get_order(wallet.wallet.user, data.id):
|
||||
return None
|
||||
if data.event_id and await get_order_by_event_id(
|
||||
wallet.wallet.user, data.event_id
|
||||
):
|
||||
return None
|
||||
|
||||
merchant = await get_merchant_for_user(wallet.wallet.user)
|
||||
assert merchant, "Cannot find merchant!"
|
||||
|
||||
products = await get_products_by_ids(
|
||||
wallet.wallet.user, [p.product_id for p in data.items]
|
||||
)
|
||||
data.validate_order_items(products)
|
||||
|
||||
total_amount = await data.total_sats(products)
|
||||
|
||||
wallet_id = await get_wallet_for_product(data.items[0].product_id)
|
||||
assert wallet_id, "Missing wallet for order `{data.id}`"
|
||||
|
||||
payment_hash, invoice = await create_invoice(
|
||||
wallet_id=wallet_id,
|
||||
amount=round(total_amount),
|
||||
memo=f"Order '{data.id}' for pubkey '{data.pubkey}'",
|
||||
extra={
|
||||
"tag": "nostrmarket",
|
||||
"order_id": data.id,
|
||||
"merchant_pubkey": merchant.public_key,
|
||||
},
|
||||
)
|
||||
|
||||
order = Order(
|
||||
**data.dict(),
|
||||
stall_id=products[0].stall_id,
|
||||
invoice_id=payment_hash,
|
||||
total=total_amount,
|
||||
extra=await OrderExtra.from_products(products),
|
||||
)
|
||||
await create_order(wallet.wallet.user, order)
|
||||
|
||||
return PaymentRequest(
|
||||
id=data.id, payment_options=[PaymentOption(type="ln", link=invoice)]
|
||||
)
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail="Cannot create order",
|
||||
)
|
||||
|
||||
|
||||
nostrmarket_ext.get("/api/v1/order/{order_id}")
|
||||
|
||||
|
||||
async def api_get_order(order_id: str, wallet: WalletTypeInfo = Depends(get_key_type)):
|
||||
try:
|
||||
order = await get_order(wallet.wallet.user, order_id)
|
||||
merchant = await get_merchant_for_user(wallet.wallet.user)
|
||||
assert merchant, "Merchant cannot be found"
|
||||
|
||||
order = await get_order(merchant.id, order_id)
|
||||
if not order:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND,
|
||||
|
|
@ -535,7 +540,10 @@ async def api_get_order(order_id: str, wallet: WalletTypeInfo = Depends(get_key_
|
|||
@nostrmarket_ext.get("/api/v1/order")
|
||||
async def api_get_orders(wallet: WalletTypeInfo = Depends(get_key_type)):
|
||||
try:
|
||||
orders = await get_orders(wallet.wallet.user)
|
||||
merchant = await get_merchant_for_user(wallet.wallet.user)
|
||||
assert merchant, "Merchant cannot be found"
|
||||
|
||||
orders = await get_orders(merchant.id)
|
||||
return orders
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
|
|
@ -552,18 +560,19 @@ async def api_update_order_status(
|
|||
) -> Order:
|
||||
try:
|
||||
assert data.shipped != None, "Shipped value is required for order"
|
||||
order = await update_order_shipped_status(
|
||||
wallet.wallet.user, data.id, data.shipped
|
||||
)
|
||||
merchant = await get_merchant_for_user(wallet.wallet.user)
|
||||
assert merchant, "Merchant cannot be found"
|
||||
|
||||
order = await update_order_shipped_status(merchant.id, data.id, data.shipped)
|
||||
assert order, "Cannot find updated order"
|
||||
|
||||
merchant = await get_merchant_for_user(wallet.wallet.user)
|
||||
merchant = await get_merchant_for_user(merchant.id)
|
||||
assert merchant, f"Merchant cannot be found for order {data.id}"
|
||||
|
||||
data.paid = order.paid
|
||||
dm_content = json.dumps(data.dict(), separators=(",", ":"), ensure_ascii=False)
|
||||
|
||||
dm_event = merchant.build_dm_event(dm_content, order.pubkey)
|
||||
dm_event = merchant.build_dm_event(dm_content, order.public_key)
|
||||
await publish_nostr_event(dm_event)
|
||||
|
||||
return order
|
||||
|
|
@ -576,6 +585,51 @@ async def api_update_order_status(
|
|||
)
|
||||
|
||||
|
||||
######################################## DIRECT MESSAGES ########################################
|
||||
|
||||
|
||||
@nostrmarket_ext.get("/api/v1/message/{public_key}")
|
||||
async def api_get_messages(
|
||||
public_key: str, wallet: WalletTypeInfo = Depends(get_key_type)
|
||||
) -> List[DirectMessage]:
|
||||
try:
|
||||
merchant = await get_merchant_for_user(wallet.wallet.user)
|
||||
assert merchant, f"Merchant cannot be found"
|
||||
|
||||
messages = await get_direct_messages(merchant.id, public_key)
|
||||
return messages
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail="Cannot get direct message",
|
||||
)
|
||||
|
||||
|
||||
@nostrmarket_ext.post("/api/v1/message")
|
||||
async def api_create_message(
|
||||
data: PartialDirectMessage, wallet: WalletTypeInfo = Depends(require_admin_key)
|
||||
) -> DirectMessage:
|
||||
try:
|
||||
merchant = await get_merchant_for_user(wallet.wallet.user)
|
||||
assert merchant, f"Merchant cannot be found"
|
||||
|
||||
dm_event = merchant.build_dm_event(data.message, data.public_key)
|
||||
data.event_id = dm_event.id
|
||||
data.event_created_at = dm_event.created_at
|
||||
|
||||
dm = await create_direct_message(merchant.id, data)
|
||||
await publish_nostr_event(dm_event)
|
||||
|
||||
return dm
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail="Cannot create message",
|
||||
)
|
||||
|
||||
|
||||
######################################## OTHER ########################################
|
||||
|
||||
|
||||
|
|
@ -593,22 +647,3 @@ async def api_stop(wallet: WalletTypeInfo = Depends(check_admin)):
|
|||
logger.warning(ex)
|
||||
|
||||
return {"success": True}
|
||||
|
||||
######################################## HELPERS ########################################
|
||||
|
||||
|
||||
async def sign_and_send_to_nostr(
|
||||
user_id: str, n: Nostrable, delete=False
|
||||
) -> NostrEvent:
|
||||
merchant = await get_merchant_for_user(user_id)
|
||||
assert merchant, "Cannot find merchant!"
|
||||
|
||||
event = (
|
||||
n.to_nostr_delete_event(merchant.public_key)
|
||||
if delete
|
||||
else n.to_nostr_event(merchant.public_key)
|
||||
)
|
||||
event.sig = merchant.sign_hash(bytes.fromhex(event.id))
|
||||
await publish_nostr_event(event)
|
||||
|
||||
return event
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue