diff --git a/__init__.py b/__init__.py index b4f7af0..4a2d0e7 100644 --- a/__init__.py +++ b/__init__.py @@ -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() diff --git a/crud.py b/crud.py index 2e6013b..f3b7fa6 100644 --- a/crud.py +++ b/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,), + ) diff --git a/helpers.py b/helpers.py index ee73cd9..d06598d 100644 --- a/helpers.py +++ b/helpers.py @@ -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) diff --git a/migrations.py b/migrations.py index d6c3ffe..530f12e 100644 --- a/migrations.py +++ b/migrations.py @@ -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)" ) diff --git a/models.py b/models.py index f228ed5..379d3f8 100644 --- a/models.py +++ b/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 diff --git a/nostr/nostr_client.py b/nostr/nostr_client.py index 3e8a47e..d5bfd7a 100644 --- a/nostr/nostr_client.py +++ b/nostr/nostr_client.py @@ -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) diff --git a/services.py b/services.py new file mode 100644 index 0000000..1e481ec --- /dev/null +++ b/services.py @@ -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 diff --git a/static/components/customer-stall/customer-stall.js b/static/components/customer-stall/customer-stall.js index d6a114a..d0b2d32 100644 --- a/static/components/customer-stall/customer-stall.js +++ b/static/components/customer-stall/customer-stall.js @@ -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 diff --git a/static/components/direct-messages/direct-messages.html b/static/components/direct-messages/direct-messages.html new file mode 100644 index 0000000..17cf401 --- /dev/null +++ b/static/components/direct-messages/direct-messages.html @@ -0,0 +1,59 @@ +
+ + +
Messages
+
+ + + + + + + +
+
+
+ +
+
+ + + + + + + +
+
+
+
diff --git a/static/components/direct-messages/direct-messages.js b/static/components/direct-messages/direct-messages.js new file mode 100644 index 0000000..3ea4787 --- /dev/null +++ b/static/components/direct-messages/direct-messages.js @@ -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() + } + }) +} diff --git a/static/components/merchant-details/merchant-details.html b/static/components/merchant-details/merchant-details.html new file mode 100644 index 0000000..0d8e1ec --- /dev/null +++ b/static/components/merchant-details/merchant-details.html @@ -0,0 +1,37 @@ +
+ + + + + Merchant Profile + Edit the merchand name, description, etc + + + + + Show Keys + Hide Keys + Show merchant public and private keys + + + + + Delete Merchant + Delete all stalls, products and orders + + + +
diff --git a/static/components/merchant-details/merchant-details.js b/static/components/merchant-details/merchant-details.js new file mode 100644 index 0000000..bc53077 --- /dev/null +++ b/static/components/merchant-details/merchant-details.js @@ -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 () {} + }) +} diff --git a/static/components/order-list/order-list.html b/static/components/order-list/order-list.html index e629b1f..cdac035 100644 --- a/static/components/order-list/order-list.html +++ b/static/components/order-list/order-list.html @@ -43,8 +43,8 @@ > - - {{toShortId(props.row.pubkey)}} + + {{toShortId(props.row.public_key)}} {{formatDate(props.row.time)}} @@ -115,7 +115,7 @@ dense readonly disabled - v-model.trim="props.row.pubkey" + v-model.trim="props.row.public_key" type="text" > diff --git a/static/components/shipping-zones/shipping-zones.html b/static/components/shipping-zones/shipping-zones.html index cedaca7..04f5650 100644 --- a/static/components/shipping-zones/shipping-zones.html +++ b/static/components/shipping-zones/shipping-zones.html @@ -4,6 +4,7 @@ unelevated color="primary" icon="public" + label="Zones" @click="openZoneDialog()" > diff --git a/static/components/stall-list/stall-list.html b/static/components/stall-list/stall-list.html index bc6236e..ea738bf 100644 --- a/static/components/stall-list/stall-list.html +++ b/static/components/stall-list/stall-list.html @@ -6,7 +6,7 @@ unelevated color="green" class="float-left" - >New StallNew Stall (Store) { 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) diff --git a/tasks.py b/tasks.py index c97c2b2..57cc36f 100644 --- a/tasks.py +++ b/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) diff --git a/templates/nostrmarket/_api_docs.html b/templates/nostrmarket/_api_docs.html index 07e30eb..772b6fc 100644 --- a/templates/nostrmarket/_api_docs.html +++ b/templates/nostrmarket/_api_docs.html @@ -27,8 +27,6 @@ >

-
-
- +
+ + +
+
+ +
+
+
+ +
+
+
+ + + +
+ + + + + +
+ Wellcome to Nostr Market!
In Nostr Market, merchant and customer communicate via NOSTR relays, so @@ -57,62 +96,31 @@
-
- - -
-
-
- -
-
- - Show Public and Private keys - -
-
-
- - - -
- - - - - -
- - -
- {{SITE_TITLE}} Nostr Market Extension -
-
- - - {% include "nostrmarket/_api_docs.html" %} - -
+
+ + +
+ {{SITE_TITLE}} Nostr Market Extension +
+
+ + + {% include "nostrmarket/_api_docs.html" %} + +
+
+
+ + +
+
@@ -141,6 +149,23 @@
{% endblock%}{% block scripts %} {{ window_vars(user) }} + + + @@ -151,6 +176,8 @@ + + {% endblock %} diff --git a/views_api.py b/views_api.py index 6d561d8..069e1fa 100644 --- a/views_api.py +++ b/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