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 @@ +