Merge pull request #15 from lnbits/customer_chat

Customer chat
This commit is contained in:
Vlad Stan 2023-03-14 16:46:09 +02:00 committed by GitHub
commit 1660ef6ef8
20 changed files with 997 additions and 430 deletions

View file

@ -42,9 +42,13 @@ from .views_api import * # noqa
def nostrmarket_start(): def nostrmarket_start():
async def _subscribe_to_nostr_client(): 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) await subscribe_to_nostr_client(recieve_event_queue, send_req_queue)
async def _wait_for_nostr_events(): 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) await wait_for_nostr_events(recieve_event_queue, send_req_queue)
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()

301
crud.py
View file

@ -5,8 +5,10 @@ from lnbits.helpers import urlsafe_short_hash
from . import db from . import db
from .models import ( from .models import (
DirectMessage,
Merchant, Merchant,
Order, Order,
PartialDirectMessage,
PartialMerchant, PartialMerchant,
PartialProduct, PartialProduct,
PartialStall, 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 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 ######################################## ######################################## 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() zone_id = urlsafe_short_hash()
await db.execute( await db.execute(
f""" f"""
INSERT INTO nostrmarket.zones (id, user_id, name, currency, cost, regions) INSERT INTO nostrmarket.zones (id, merchant_id, name, currency, cost, regions)
VALUES (?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?)
""", """,
( (
zone_id, zone_id,
user_id, merchant_id,
data.name, data.name,
data.currency, data.currency,
data.cost, 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" assert zone, "Newly created zone couldn't be retrieved"
return zone 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( await db.execute(
f"UPDATE nostrmarket.zones SET name = ?, cost = ?, regions = ? WHERE id = ? AND 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, user_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( 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, zone_id,
), ),
) )
return Zone.from_row(row) if row else None 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( 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] return [Zone.from_row(row) for row in rows]
async def delete_zone(zone_id: str) -> None: async def delete_zone(merchant_id: str, zone_id: str) -> None:
# todo: add user_id
await db.execute("DELETE FROM nostrmarket.zones WHERE id = ?", (zone_id,)) 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 ######################################## ######################################## 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() stall_id = urlsafe_short_hash()
await db.execute( await db.execute(
f""" 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 (?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?)
""", """,
( (
user_id, merchant_id,
stall_id, stall_id,
data.wallet, data.wallet,
data.name, 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" assert stall, "Newly created stall couldn't be retrieved"
return stall 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( 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, stall_id,
), ),
) )
return Stall.from_row(row) if row else None 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( rows = await db.fetchall(
"SELECT * FROM nostrmarket.stalls WHERE user_id = ?", "SELECT * FROM nostrmarket.stalls WHERE merchant_id = ?",
(user_id,), (merchant_id,),
) )
return [Stall.from_row(row) for row in rows] 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( await db.execute(
f""" f"""
UPDATE nostrmarket.stalls SET wallet = ?, name = ?, currency = ?, zones = ?, meta = ? UPDATE nostrmarket.stalls SET wallet = ?, name = ?, currency = ?, zones = ?, meta = ?
WHERE user_id = ? AND id = ? WHERE merchant_id = ? AND id = ?
""", """,
( (
stall.wallet, 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] [z.dict() for z in stall.shipping_zones]
), # todo: cost is float. should be int for sats ), # todo: cost is float. should be int for sats
json.dumps(stall.config.dict()), json.dumps(stall.config.dict()),
user_id, merchant_id,
stall.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( 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, stall_id,
), ),
) )
async def delete_merchant_stalls(merchant_id: str) -> None:
await db.execute(
"DELETE FROM nostrmarket.stalls WHERE merchant_id = ?",
(merchant_id,),
)
######################################## PRODUCTS ######################################## ######################################## 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() product_id = urlsafe_short_hash()
await db.execute( await db.execute(
f""" 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 (?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""", """,
( (
user_id, merchant_id,
product_id, product_id,
data.stall_id, data.stall_id,
data.name, data.name,
@ -229,18 +257,18 @@ async def create_product(user_id: str, data: PartialProduct) -> Product:
json.dumps(data.config.dict()), 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" assert product, "Newly created product couldn't be retrieved"
return product 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( await db.execute(
f""" f"""
UPDATE nostrmarket.products set name = ?, image = ?, price = ?, quantity = ?, category_list = ?, meta = ? UPDATE nostrmarket.products set name = ?, image = ?, price = ?, quantity = ?, category_list = ?, meta = ?
WHERE user_id = ? AND id = ? WHERE merchant_id = ? AND id = ?
""", """,
( (
product.name, product.name,
@ -249,40 +277,42 @@ async def update_product(user_id: str, product: Product) -> Product:
product.quantity, product.quantity,
json.dumps(product.categories), json.dumps(product.categories),
json.dumps(product.config.dict()), json.dumps(product.config.dict()),
user_id, merchant_id,
product.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" assert updated_product, "Updated product couldn't be retrieved"
return updated_product 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( 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, product_id,
), ),
) )
return Product.from_row(row) if row else None 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( rows = await db.fetchall(
"SELECT * FROM nostrmarket.products WHERE user_id = ? AND stall_id = ?", "SELECT * FROM nostrmarket.products WHERE merchant_id = ? AND stall_id = ?",
(user_id, stall_id), (merchant_id, stall_id),
) )
return [Product.from_row(row) for row in rows] 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)) q = ",".join(["?"] * len(product_ids))
rows = await db.fetchall( 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})", f"SELECT id, stall_id, name, price, quantity, category_list, meta FROM nostrmarket.products WHERE merchant_id = ? AND id IN ({q})",
(user_id, *product_ids), (merchant_id, *product_ids),
) )
return [Product.from_row(row) for row in rows] 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 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( 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, product_id,
), ),
) )
async def delete_merchant_products(merchant_id: str) -> None:
await db.execute(
"DELETE FROM nostrmarket.products WHERE merchant_id = ?",
(merchant_id,),
)
######################################## ORDERS ######################################## ######################################## ORDERS ########################################
async def create_order(user_id: str, o: Order) -> Order: async def create_order(merchant_id: str, o: Order) -> Order:
await db.execute( await db.execute(
f""" f"""
INSERT INTO nostrmarket.orders (user_id, id, event_id, pubkey, address, contact_data, extra_data, order_items, stall_id, invoice_id, total) INSERT INTO nostrmarket.orders (
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) 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.id,
o.event_id, o.event_id,
o.pubkey, o.event_created_at,
o.merchant_public_key,
o.public_key,
o.address, o.address,
json.dumps(o.contact.dict() if o.contact else {}), json.dumps(o.contact.dict() if o.contact else {}),
json.dumps(o.extra.dict()), json.dumps(o.extra.dict()),
@ -333,53 +387,64 @@ async def create_order(user_id: str, o: Order) -> Order:
o.total, 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" assert order, "Newly created order couldn't be retrieved"
return order 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( 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, order_id,
), ),
) )
return Order.from_row(row) if row else None 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( 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, event_id,
), ),
) )
return Order.from_row(row) if row else None 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( rows = await db.fetchall(
"SELECT * FROM nostrmarket.orders WHERE user_id = ? ORDER BY time DESC", "SELECT * FROM nostrmarket.orders WHERE merchant_id = ? ORDER BY time DESC",
(user_id,), (merchant_id,),
) )
return [Order.from_row(row) for row in rows] 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( 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, stall_id,
), ),
) )
return [Order.from_row(row) for row in rows] 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]: async def update_order_paid_status(order_id: str, paid: bool) -> Optional[Order]:
await db.execute( await db.execute(
f"UPDATE nostrmarket.orders SET paid = ? WHERE id = ?", 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( async def update_order_shipped_status(
user_id: str, order_id: str, shipped: bool merchant_id: str, order_id: str, shipped: bool
) -> Optional[Order]: ) -> Optional[Order]:
await db.execute( await db.execute(
f"UPDATE nostrmarket.orders SET shipped = ? WHERE user_id = ? AND id = ?", f"UPDATE nostrmarket.orders SET shipped = ? WHERE merchant_id = ? AND id = ?",
(shipped, user_id, order_id), (shipped, merchant_id, order_id),
) )
row = await db.fetchone( row = await db.fetchone(
@ -405,3 +470,91 @@ async def update_order_shipped_status(
(order_id,), (order_id,),
) )
return Order.from_row(row) if row else None 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,),
)

View file

@ -16,6 +16,8 @@ def get_shared_secret(privkey: str, pubkey: str):
def decrypt_message(encoded_message: str, encryption_key) -> str: def decrypt_message(encoded_message: str, encryption_key) -> str:
encoded_data = encoded_message.split("?iv=") 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] encoded_content, encoded_iv = encoded_data[0], encoded_data[1]
iv = base64.b64decode(encoded_iv) iv = base64.b64decode(encoded_iv)

View file

@ -18,11 +18,11 @@ async def m001_initial(db):
""" """
Initial stalls table. Initial stalls table.
""" """
# user_id, id, wallet, name, currency, zones, meta
await db.execute( await db.execute(
""" """
CREATE TABLE nostrmarket.stalls ( CREATE TABLE nostrmarket.stalls (
user_id TEXT NOT NULL, merchant_id TEXT NOT NULL,
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
wallet TEXT NOT NULL, wallet TEXT NOT NULL,
name TEXT NOT NULL, name TEXT NOT NULL,
@ -39,7 +39,7 @@ async def m001_initial(db):
await db.execute( await db.execute(
""" """
CREATE TABLE nostrmarket.products ( CREATE TABLE nostrmarket.products (
user_id TEXT NOT NULL, merchant_id TEXT NOT NULL,
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
stall_id TEXT NOT NULL, stall_id TEXT NOT NULL,
name TEXT NOT NULL, name TEXT NOT NULL,
@ -59,7 +59,7 @@ async def m001_initial(db):
""" """
CREATE TABLE nostrmarket.zones ( CREATE TABLE nostrmarket.zones (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
user_id TEXT NOT NULL, merchant_id TEXT NOT NULL,
name TEXT NOT NULL, name TEXT NOT NULL,
currency TEXT NOT NULL, currency TEXT NOT NULL,
cost REAL NOT NULL, cost REAL NOT NULL,
@ -75,10 +75,12 @@ async def m001_initial(db):
await db.execute( await db.execute(
f""" f"""
CREATE TABLE nostrmarket.orders ( CREATE TABLE nostrmarket.orders (
user_id TEXT NOT NULL, merchant_id TEXT NOT NULL,
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
event_id TEXT, 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}', contact_data TEXT NOT NULL DEFAULT '{empty_object}',
extra_data TEXT NOT NULL DEFAULT '{empty_object}', extra_data TEXT NOT NULL DEFAULT '{empty_object}',
order_items TEXT NOT NULL, order_items TEXT NOT NULL,
@ -88,20 +90,8 @@ async def m001_initial(db):
invoice_id TEXT NOT NULL, invoice_id TEXT NOT NULL,
paid BOOLEAN NOT NULL DEFAULT false, paid BOOLEAN NOT NULL DEFAULT false,
shipped BOOLEAN NOT NULL DEFAULT false, shipped BOOLEAN NOT NULL DEFAULT false,
time TIMESTAMP NOT NULL DEFAULT {db.timestamp_now} time TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
); UNIQUE(event_id)
"""
)
"""
Initial market table.
"""
await db.execute(
"""
CREATE TABLE nostrmarket.markets (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
name TEXT
); );
""" """
) )
@ -111,13 +101,17 @@ async def m001_initial(db):
""" """
await db.execute( await db.execute(
f""" f"""
CREATE TABLE nostrmarket.messages ( CREATE TABLE nostrmarket.direct_messages (
merchant_id TEXT NOT NULL,
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
msg TEXT NOT NULL, event_id TEXT,
pubkey TEXT NOT NULL, event_created_at INTEGER NOT NULL,
conversation_id TEXT NOT NULL, message TEXT NOT NULL,
timestamp TIMESTAMP NOT NULL DEFAULT {db.timestamp_now} 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 Create indexes for message fetching
""" """
await db.execute( 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( await db.execute(
"CREATE INDEX idx_messages_conversations ON nostrmarket.messages (conversation_id)" "CREATE INDEX idx_event_id ON nostrmarket.direct_messages (event_id)"
) )

View file

@ -281,7 +281,9 @@ class OrderExtra(BaseModel):
class PartialOrder(BaseModel): class PartialOrder(BaseModel):
id: str id: str
event_id: Optional[str] event_id: Optional[str]
pubkey: str event_created_at: Optional[int]
public_key: str
merchant_public_key: str
items: List[OrderItem] items: List[OrderItem]
contact: Optional[OrderContact] contact: Optional[OrderContact]
address: Optional[str] address: Optional[str]
@ -359,3 +361,24 @@ class PaymentRequest(BaseModel):
id: str id: str
message: Optional[str] message: Optional[str]
payment_options: List[PaymentOption] 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

View file

@ -1,28 +1,18 @@
from threading import Thread from threading import Thread
from typing import Callable from typing import Callable
import httpx
from loguru import logger from loguru import logger
from websocket import WebSocketApp from websocket import WebSocketApp
from lnbits.app import settings from lnbits.app import settings
from lnbits.helpers import url_for
from .. import send_req_queue
from .event import NostrEvent from .event import NostrEvent
async def publish_nostr_event(e: NostrEvent): async def publish_nostr_event(e: NostrEvent):
url = url_for("/nostrclient/api/v1/publish", external=True) print("### publish_nostr_event", e.dict())
data = dict(e) await send_req_queue.put(["EVENT", e.dict()])
print("### published", dict(data))
async with httpx.AsyncClient() as client:
try:
await client.post(
url,
json=data,
)
except Exception as ex:
logger.warning(ex)
async def connect_to_nostrclient_ws( 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") logger.debug(f"Subscribing to websockets for nostrclient extension")
ws = WebSocketApp( 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_message=on_message,
on_open=on_open, on_open=on_open,
on_error=on_error, on_error=on_error,
@ -44,23 +34,3 @@ async def connect_to_nostrclient_ws(
wst.start() wst.start()
return ws 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
View 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

View file

@ -345,8 +345,9 @@ async function customerStall(path) {
let json = JSON.parse(text) let json = JSON.parse(text)
if (json.id != this.activeOrder) return if (json.id != this.activeOrder) return
if (json.payment_options) { if (json.payment_options) {
let payment_request = json.payment_options.find(o => o.type == 'ln') let payment_request = json.payment_options.find(
.link o => o.type == 'ln'
).link
if (!payment_request) return if (!payment_request) return
this.loading = false this.loading = false
this.qrCodeDialog.data.payment_request = payment_request this.qrCodeDialog.data.payment_request = payment_request

View 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>

View 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()
}
})
}

View 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>

View 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 () {}
})
}

View file

@ -43,8 +43,8 @@
></q-checkbox> ></q-checkbox>
</q-td> </q-td>
<q-td key="pubkey" :props="props"> <q-td key="public_key" :props="props">
{{toShortId(props.row.pubkey)}} {{toShortId(props.row.public_key)}}
</q-td> </q-td>
<q-td key="time" :props="props"> {{formatDate(props.row.time)}} </q-td> <q-td key="time" :props="props"> {{formatDate(props.row.time)}} </q-td>
</q-tr> </q-tr>
@ -115,7 +115,7 @@
dense dense
readonly readonly
disabled disabled
v-model.trim="props.row.pubkey" v-model.trim="props.row.public_key"
type="text" type="text"
></q-input> ></q-input>
</div> </div>

View file

@ -4,6 +4,7 @@
unelevated unelevated
color="primary" color="primary"
icon="public" icon="public"
label="Zones"
@click="openZoneDialog()" @click="openZoneDialog()"
> >
<q-list> <q-list>

View file

@ -6,7 +6,7 @@
unelevated unelevated
color="green" color="green"
class="float-left" class="float-left"
>New Stall</q-btn >New Stall (Store)</q-btn
> >
<q-input <q-input
borderless borderless

View file

@ -6,6 +6,10 @@ const merchant = async () => {
await stallDetails('static/components/stall-details/stall-details.html') await stallDetails('static/components/stall-details/stall-details.html')
await stallList('static/components/stall-list/stall-list.html') await stallList('static/components/stall-list/stall-list.html')
await orderList('static/components/order-list/order-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 const nostr = window.NostrTools
@ -51,6 +55,9 @@ const merchant = async () => {
showImportKeysDialog: async function () { showImportKeysDialog: async function () {
this.importKeyDialog.show = true this.importKeyDialog.show = true
}, },
toggleMerchantKeys: function (value) {
this.showKeys = value
},
createMerchant: async function (privateKey) { createMerchant: async function (privateKey) {
try { try {
const pubkey = nostr.getPublicKey(privateKey) const pubkey = nostr.getPublicKey(privateKey)

127
tasks.py
View file

@ -2,26 +2,20 @@ import asyncio
import json import json
from asyncio import Queue from asyncio import Queue
import httpx
import websocket import websocket
from loguru import logger from loguru import logger
from websocket import WebSocketApp from websocket import WebSocketApp
from lnbits.core import get_wallet
from lnbits.core.models import Payment from lnbits.core.models import Payment
from lnbits.helpers import Optional, url_for
from lnbits.tasks import register_invoice_listener from lnbits.tasks import register_invoice_listener
from .crud import ( from .crud import (
get_merchant_by_pubkey, get_last_direct_messages_time,
get_last_order_time,
get_public_keys_for_merchants, get_public_keys_for_merchants,
get_wallet_for_product,
update_order_paid_status,
) )
from .helpers import order_from_json from .nostr.nostr_client import connect_to_nostrclient_ws
from .models import OrderStatusUpdate, PartialOrder from .services import handle_order_paid, process_nostr_message
from .nostr.event import NostrEvent
from .nostr.nostr_client import connect_to_nostrclient_ws, publish_nostr_event
async def wait_for_paid_invoices(): 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) 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): async def subscribe_to_nostr_client(recieve_event_queue: Queue, send_req_queue: Queue):
print("### subscribe_nostrclient_ws")
def on_open(_): def on_open(_):
logger.info("Connected to 'nostrclient' websocket") logger.info("Connected to 'nostrclient' websocket")
def on_message(_, message): def on_message(_, message):
print("### on_message", message) # print("### on_message", message)
recieve_event_queue.put_nowait(message) recieve_event_queue.put_nowait(message)
# wait for 'nostrclient' extension to initialize
await asyncio.sleep(5)
ws: WebSocketApp = None ws: WebSocketApp = None
while True: while True:
try: 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): async def wait_for_nostr_events(recieve_event_queue: Queue, send_req_queue: Queue):
public_keys = await get_public_keys_for_merchants() public_keys = await get_public_keys_for_merchants()
for p in public_keys: 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( 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: while True:
message = await recieve_event_queue.get() message = await recieve_event_queue.get()
await handle_message(message) await process_nostr_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

View file

@ -27,8 +27,6 @@
></small ></small
> >
</p> </p>
<br />
<br />
<a <a
class="text-secondary" class="text-secondary"
target="_blank" target="_blank"

View file

@ -2,7 +2,46 @@
%} {% block page %} %} {% block page %}
<div class="row q-col-gutter-md"> <div class="row q-col-gutter-md">
<div class="col-12 col-md-7 q-gutter-y-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> <q-card-section>
<span class="text-h4">Wellcome to Nostr Market!</span><br /> <span class="text-h4">Wellcome to Nostr Market!</span><br />
In Nostr Market, merchant and customer communicate via NOSTR relays, so In Nostr Market, merchant and customer communicate via NOSTR relays, so
@ -57,62 +96,31 @@
</div> </div>
</q-card-section> </q-card-section>
</q-card> </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>
<div class="col-12 col-md-5 q-gutter-y-md"> <div class="col-12 col-md-5 q-gutter-y-md">
<q-card> <div class="col-12">
<q-card-section> <q-card>
<h6 class="text-subtitle1 q-my-none"> <q-card-section>
{{SITE_TITLE}} Nostr Market Extension <h6 class="text-subtitle1 q-my-none">
</h6> {{SITE_TITLE}} Nostr Market Extension
</q-card-section> </h6>
<q-card-section class="q-pa-none"> </q-card-section>
<q-separator></q-separator> <q-card-section class="q-pa-none">
<q-list> {% include "nostrmarket/_api_docs.html" %} </q-list> <q-separator></q-separator>
</q-card-section> <q-list> {% include "nostrmarket/_api_docs.html" %} </q-list>
</q-card> </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>
<div> <div>
<q-dialog v-model="importKeyDialog.show" position="top"> <q-dialog v-model="importKeyDialog.show" position="top">
<q-card class="q-pa-lg q-pt-xl" style="width: 500px"> <q-card class="q-pa-lg q-pt-xl" style="width: 500px">
@ -141,6 +149,23 @@
</div> </div>
</div> </div>
{% endblock%}{% block scripts %} {{ window_vars(user) }} {% 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 --> <!-- todo: serve locally -->
<script src="https://unpkg.com/nostr-tools/lib/nostr.bundle.js"></script> <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> <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-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/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/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> <script src="{{ url_for('nostrmarket_static', path='js/index.js') }}"></script>
{% endblock %} {% endblock %}

View file

@ -6,7 +6,6 @@ from fastapi import Depends
from fastapi.exceptions import HTTPException from fastapi.exceptions import HTTPException
from loguru import logger from loguru import logger
from lnbits.core import create_invoice
from lnbits.decorators import ( from lnbits.decorators import (
WalletTypeInfo, WalletTypeInfo,
check_admin, check_admin,
@ -18,25 +17,29 @@ from lnbits.utils.exchange_rates import currencies
from . import nostrmarket_ext, scheduled_tasks from . import nostrmarket_ext, scheduled_tasks
from .crud import ( from .crud import (
create_direct_message,
create_merchant, create_merchant,
create_order,
create_product, create_product,
create_stall, create_stall,
create_zone, create_zone,
delete_merchant_direct_messages,
delete_merchant_orders,
delete_merchant_products,
delete_merchant_stalls,
delete_merchant_zones,
delete_merchants,
delete_product, delete_product,
delete_stall, delete_stall,
delete_zone, delete_zone,
get_direct_messages,
get_merchant_for_user, get_merchant_for_user,
get_order, get_order,
get_order_by_event_id,
get_orders, get_orders,
get_orders_for_stall, get_orders_for_stall,
get_product, get_product,
get_products, get_products,
get_products_by_ids,
get_stall, get_stall,
get_stalls, get_stalls,
get_wallet_for_product,
get_zone, get_zone,
get_zones, get_zones,
update_order_shipped_status, update_order_shipped_status,
@ -45,24 +48,21 @@ from .crud import (
update_zone, update_zone,
) )
from .models import ( from .models import (
DirectMessage,
Merchant, Merchant,
Nostrable,
Order, Order,
OrderExtra,
OrderStatusUpdate, OrderStatusUpdate,
PartialDirectMessage,
PartialMerchant, PartialMerchant,
PartialOrder,
PartialProduct, PartialProduct,
PartialStall, PartialStall,
PartialZone, PartialZone,
PaymentOption,
PaymentRequest,
Product, Product,
Stall, Stall,
Zone, Zone,
) )
from .nostr.event import NostrEvent
from .nostr.nostr_client import publish_nostr_event from .nostr.nostr_client import publish_nostr_event
from .services import sign_and_send_to_nostr
######################################## MERCHANT ######################################## ######################################## MERCHANT ########################################
@ -75,6 +75,7 @@ async def api_create_merchant(
try: try:
merchant = await create_merchant(wallet.wallet.user, data) merchant = await create_merchant(wallet.wallet.user, data)
return merchant return merchant
except Exception as ex: except Exception as ex:
logger.warning(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 ######################################## ######################################## ZONES ########################################
@nostrmarket_ext.get("/api/v1/zone") @nostrmarket_ext.get("/api/v1/zone")
async def api_get_zones(wallet: WalletTypeInfo = Depends(get_key_type)) -> List[Zone]: async def api_get_zones(wallet: WalletTypeInfo = Depends(get_key_type)) -> List[Zone]:
try: 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: except Exception as ex:
logger.warning(ex) logger.warning(ex)
raise HTTPException( raise HTTPException(
@ -120,7 +148,9 @@ async def api_create_zone(
data: PartialZone, wallet: WalletTypeInfo = Depends(require_admin_key) data: PartialZone, wallet: WalletTypeInfo = Depends(require_admin_key)
): ):
try: 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 return zone
except Exception as ex: except Exception as ex:
logger.warning(ex) logger.warning(ex)
@ -137,7 +167,9 @@ async def api_update_zone(
wallet: WalletTypeInfo = Depends(require_admin_key), wallet: WalletTypeInfo = Depends(require_admin_key),
) -> Zone: ) -> Zone:
try: 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: if not zone:
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, status_code=HTTPStatus.NOT_FOUND,
@ -159,7 +191,9 @@ async def api_update_zone(
@nostrmarket_ext.delete("/api/v1/zone/{zone_id}") @nostrmarket_ext.delete("/api/v1/zone/{zone_id}")
async def api_delete_zone(zone_id, wallet: WalletTypeInfo = Depends(require_admin_key)): async def api_delete_zone(zone_id, wallet: WalletTypeInfo = Depends(require_admin_key)):
try: 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: if not zone:
raise HTTPException( raise HTTPException(
@ -167,7 +201,7 @@ async def api_delete_zone(zone_id, wallet: WalletTypeInfo = Depends(require_admi
detail="Zone does not exist.", detail="Zone does not exist.",
) )
await delete_zone(zone_id) await delete_zone(wallet.wallet.user, zone_id)
except Exception as ex: except Exception as ex:
logger.warning(ex) logger.warning(ex)
@ -188,12 +222,14 @@ async def api_create_stall(
try: try:
data.validate_stall() 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 stall.config.event_id = event.id
await update_stall(wallet.wallet.user, stall) await update_stall(merchant.id, stall)
return stall return stall
except ValueError as ex: except ValueError as ex:
@ -217,13 +253,16 @@ async def api_update_stall(
try: try:
data.validate_stall() 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" 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 stall.config.event_id = event.id
await update_stall(wallet.wallet.user, stall) await update_stall(merchant.id, stall)
return stall return stall
except HTTPException as ex: except HTTPException as ex:
@ -244,7 +283,9 @@ async def api_update_stall(
@nostrmarket_ext.get("/api/v1/stall/{stall_id}") @nostrmarket_ext.get("/api/v1/stall/{stall_id}")
async def api_get_stall(stall_id: str, wallet: WalletTypeInfo = Depends(get_key_type)): async def api_get_stall(stall_id: str, wallet: WalletTypeInfo = Depends(get_key_type)):
try: 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: if not stall:
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, 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") @nostrmarket_ext.get("/api/v1/stall")
async def api_get_stalls(wallet: WalletTypeInfo = Depends(get_key_type)): async def api_get_stalls(wallet: WalletTypeInfo = Depends(get_key_type)):
try: 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 return stalls
except Exception as ex: except Exception as ex:
logger.warning(ex) logger.warning(ex)
@ -280,7 +323,9 @@ async def api_get_stall_products(
wallet: WalletTypeInfo = Depends(require_invoice_key), wallet: WalletTypeInfo = Depends(require_invoice_key),
): ):
try: 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 return products
except Exception as ex: except Exception as ex:
logger.warning(ex) logger.warning(ex)
@ -296,7 +341,9 @@ async def api_get_stall_orders(
wallet: WalletTypeInfo = Depends(require_invoice_key), wallet: WalletTypeInfo = Depends(require_invoice_key),
): ):
try: 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 return orders
except Exception as ex: except Exception as ex:
logger.warning(ex) logger.warning(ex)
@ -311,19 +358,21 @@ async def api_delete_stall(
stall_id: str, wallet: WalletTypeInfo = Depends(require_admin_key) stall_id: str, wallet: WalletTypeInfo = Depends(require_admin_key)
): ):
try: 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: if not stall:
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, status_code=HTTPStatus.NOT_FOUND,
detail="Stall does not exist.", 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 stall.config.event_id = event.id
await update_stall(wallet.wallet.user, stall) await update_stall(merchant.id, stall)
except HTTPException as ex: except HTTPException as ex:
raise ex raise ex
@ -345,17 +394,19 @@ async def api_create_product(
) -> Product: ) -> Product:
try: try:
data.validate_product() 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" assert stall, "Stall missing for product"
data.config.currency = stall.currency 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 product.config.event_id = event.id
await update_product(wallet.wallet.user, product) await update_product(merchant.id, product)
return product return product
except ValueError as ex: except ValueError as ex:
@ -382,17 +433,19 @@ async def api_update_product(
raise ValueError("Bad product ID") raise ValueError("Bad product ID")
product.validate_product() 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" assert stall, "Stall missing for product"
product.config.currency = stall.currency 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 product.config.event_id = event.id
await update_product(wallet.wallet.user, product) await update_product(merchant.id, product)
return product return product
except ValueError as ex: except ValueError as ex:
@ -414,7 +467,10 @@ async def api_get_product(
wallet: WalletTypeInfo = Depends(require_invoice_key), wallet: WalletTypeInfo = Depends(require_invoice_key),
) -> Optional[Product]: ) -> Optional[Product]:
try: 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 return products
except Exception as ex: except Exception as ex:
logger.warning(ex) logger.warning(ex)
@ -430,15 +486,18 @@ async def api_delete_product(
wallet: WalletTypeInfo = Depends(require_admin_key), wallet: WalletTypeInfo = Depends(require_admin_key),
): ):
try: 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: if not product:
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, status_code=HTTPStatus.NOT_FOUND,
detail="Product does not exist.", detail="Product does not exist.",
) )
await delete_product(wallet.wallet.user, product_id) await delete_product(merchant.id, product_id)
await sign_and_send_to_nostr(wallet.wallet.user, product, True) await sign_and_send_to_nostr(merchant, product, True)
except HTTPException as ex: except HTTPException as ex:
raise ex raise ex
@ -453,69 +512,15 @@ async def api_delete_product(
######################################## ORDERS ######################################## ######################################## 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}") nostrmarket_ext.get("/api/v1/order/{order_id}")
async def api_get_order(order_id: str, wallet: WalletTypeInfo = Depends(get_key_type)): async def api_get_order(order_id: str, wallet: WalletTypeInfo = Depends(get_key_type)):
try: 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: if not order:
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, 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") @nostrmarket_ext.get("/api/v1/order")
async def api_get_orders(wallet: WalletTypeInfo = Depends(get_key_type)): async def api_get_orders(wallet: WalletTypeInfo = Depends(get_key_type)):
try: 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 return orders
except Exception as ex: except Exception as ex:
logger.warning(ex) logger.warning(ex)
@ -552,18 +560,19 @@ async def api_update_order_status(
) -> Order: ) -> Order:
try: try:
assert data.shipped != None, "Shipped value is required for order" assert data.shipped != None, "Shipped value is required for order"
order = await update_order_shipped_status( merchant = await get_merchant_for_user(wallet.wallet.user)
wallet.wallet.user, data.id, data.shipped assert merchant, "Merchant cannot be found"
)
order = await update_order_shipped_status(merchant.id, data.id, data.shipped)
assert order, "Cannot find updated order" 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}" assert merchant, f"Merchant cannot be found for order {data.id}"
data.paid = order.paid data.paid = order.paid
dm_content = json.dumps(data.dict(), separators=(",", ":"), ensure_ascii=False) 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) await publish_nostr_event(dm_event)
return order 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 ######################################## ######################################## OTHER ########################################
@ -593,22 +647,3 @@ async def api_stop(wallet: WalletTypeInfo = Depends(check_admin)):
logger.warning(ex) logger.warning(ex)
return {"success": True} 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