From 54978c847f83f7f9b9d82cef7592fbb33b52fc91 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Fri, 3 Mar 2023 11:08:55 +0200 Subject: [PATCH 1/5] feat: publish events for products --- crud.py | 12 ++-- migrations.py | 8 +-- models.py | 41 +++++++++-- .../stall-details/stall-details.html | 2 +- .../components/stall-details/stall-details.js | 10 +-- views_api.py | 68 +++++++++++++++++-- 6 files changed, 117 insertions(+), 24 deletions(-) diff --git a/crud.py b/crud.py index 996bfc3..f4d85c5 100644 --- a/crud.py +++ b/crud.py @@ -197,7 +197,7 @@ async def create_product(user_id: str, data: PartialProduct) -> Product: await db.execute( f""" - INSERT INTO nostrmarket.products (user_id, id, stall_id, name, category_list, description, images, price, quantity) + INSERT INTO nostrmarket.products (user_id, id, stall_id, name, images, price, quantity, category_list, meta) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( @@ -205,11 +205,11 @@ async def create_product(user_id: str, data: PartialProduct) -> Product: product_id, data.stall_id, data.name, - json.dumps(data.categories), - data.description, data.image, data.price, data.quantity, + json.dumps(data.categories), + json.dumps(data.config.dict()), ), ) product = await get_product(user_id, product_id) @@ -222,18 +222,18 @@ async def update_product(user_id: str, product: Product) -> Product: await db.execute( f""" - UPDATE nostrmarket.products set name = ?, category_list = ?, description = ?, images = ?, price = ?, quantity = ? + UPDATE nostrmarket.products set name = ?, description = ?, images = ?, price = ?, quantity = ?, category_list = ?, meta = ? WHERE user_id = ? AND id = ? """, ( product.name, - json.dumps(product.categories), - product.description, product.image, product.price, product.quantity, user_id, product.id, + json.dumps(product.categories), + json.dumps(product.config), ), ) updated_product = await get_product(user_id, product.id) diff --git a/migrations.py b/migrations.py index 2e0a158..696e584 100644 --- a/migrations.py +++ b/migrations.py @@ -37,17 +37,17 @@ async def m001_initial(db): Initial products table. """ await db.execute( - f""" + """ CREATE TABLE nostrmarket.products ( user_id TEXT NOT NULL, id TEXT PRIMARY KEY, stall_id TEXT NOT NULL, name TEXT NOT NULL, - category_list TEXT DEFAULT '[]', - description TEXT, images TEXT DEFAULT '[]', price REAL NOT NULL, - quantity INTEGER NOT NULL + quantity INTEGER NOT NULL, + category_list TEXT DEFAULT '[]', + meta TEXT NOT NULL DEFAULT '{}' ); """ ) diff --git a/models.py b/models.py index 412bbfc..0becfe3 100644 --- a/models.py +++ b/models.py @@ -1,5 +1,6 @@ import json import time +from abc import abstractmethod from sqlite3 import Row from typing import List, Optional @@ -8,6 +9,18 @@ from pydantic import BaseModel from .helpers import sign_message_hash from .nostr.event import NostrEvent +######################################## NOSTR ######################################## + + +class Nostrable: + @abstractmethod + def to_nostr_event(self, pubkey: str) -> NostrEvent: + pass + + @abstractmethod + def to_nostr_delete_event(self, pubkey: str) -> NostrEvent: + pass + ######################################## MERCHANT ######################################## class MerchantConfig(BaseModel): @@ -90,7 +103,7 @@ class Stall(PartialStall): event = NostrEvent( pubkey=pubkey, created_at=round(time.time()), - kind=30005, + kind=30017, tags=[["d", self.id]], content=json.dumps(content, separators=(",", ":"), ensure_ascii=False), ) @@ -121,14 +134,19 @@ class Stall(PartialStall): ######################################## STALLS ######################################## +class ProductConfig(BaseModel): + event_id: Optional[str] + description: Optional[str] + + class PartialProduct(BaseModel): stall_id: str name: str categories: List[str] = [] - description: Optional[str] image: Optional[str] price: float quantity: int + config: ProductConfig = ProductConfig() def validate_product(self): if self.image: @@ -150,14 +168,14 @@ class PartialProduct(BaseModel): ) -class Product(PartialProduct): +class Product(PartialProduct, Nostrable): id: str def to_nostr_event(self, pubkey: str) -> NostrEvent: content = { "stall_id": self.stall_id, "name": self.name, - "description": self.description, + "description": self.config.description, "image": self.image, "price": self.price, "quantity": self.quantity, @@ -167,7 +185,7 @@ class Product(PartialProduct): event = NostrEvent( pubkey=pubkey, created_at=round(time.time()), - kind=30005, + kind=30018, tags=[["d", self.id]] + categories, content=json.dumps(content, separators=(",", ":"), ensure_ascii=False), ) @@ -175,8 +193,21 @@ class Product(PartialProduct): return event + def to_nostr_delete_event(self, pubkey: str) -> NostrEvent: + delete_event = NostrEvent( + pubkey=pubkey, + created_at=round(time.time()), + kind=5, + tags=[["e", self.config.event_id]], + content=f"Product '{self.name}' deleted", + ) + delete_event.id = delete_event.event_id + + return delete_event + @classmethod def from_row(cls, row: Row) -> "Product": product = cls(**dict(row)) + product.config = ProductConfig(**json.loads(row["meta"])) product.categories = json.loads(row["category_list"]) return product diff --git a/static/components/stall-details/stall-details.html b/static/components/stall-details/stall-details.html index 82610ca..9246aad 100644 --- a/static/components/stall-details/stall-details.html +++ b/static/components/stall-details/stall-details.html @@ -161,7 +161,7 @@ - {{props.row.description}} + {{props.row.config.description}} diff --git a/static/components/stall-details/stall-details.js b/static/components/stall-details/stall-details.js index e62caaf..b8ef9c8 100644 --- a/static/components/stall-details/stall-details.js +++ b/static/components/stall-details/stall-details.js @@ -190,7 +190,7 @@ async function stallDetails(path) { try { const {data} = await LNbits.api.request( 'GET', - '/nostrmarket/api/v1/product/' + this.stall.id, + '/nostrmarket/api/v1/stall/product/' + this.stall.id, this.inkey ) this.products = data @@ -205,12 +205,14 @@ async function stallDetails(path) { stall_id: this.stall.id, id: this.productDialog.data.id, name: this.productDialog.data.name, - description: this.productDialog.data.description, - categories: this.productDialog.data.categories, image: this.productDialog.data.image, price: this.productDialog.data.price, - quantity: this.productDialog.data.quantity + quantity: this.productDialog.data.quantity, + categories: this.productDialog.data.categories, + config: { + description: this.productDialog.data.description + } } this.productDialog.showDialog = false if (this.productDialog.data.id) { diff --git a/views_api.py b/views_api.py index 3de3f97..63965d9 100644 --- a/views_api.py +++ b/views_api.py @@ -12,6 +12,7 @@ from lnbits.decorators import ( require_admin_key, require_invoice_key, ) +from lnbits.extensions.nostrmarket.nostr.event import NostrEvent from lnbits.utils.exchange_rates import currencies from . import nostrmarket_ext @@ -24,6 +25,7 @@ from .crud import ( delete_stall, delete_zone, get_merchant_for_user, + get_product, get_products, get_stall, get_stalls, @@ -35,6 +37,7 @@ from .crud import ( ) from .models import ( Merchant, + Nostrable, PartialMerchant, PartialProduct, PartialStall, @@ -266,6 +269,22 @@ async def api_get_stalls(wallet: WalletTypeInfo = Depends(get_key_type)): ) +@nostrmarket_ext.get("/api/v1/stall/product/{stall_id}") +async def api_get_stall_products( + stall_id: str, + wallet: WalletTypeInfo = Depends(require_invoice_key), +): + try: + products = await get_products(wallet.wallet.user, stall_id) + return products + except Exception as ex: + logger.warning(ex) + raise HTTPException( + status_code=HTTPStatus.INTERNAL_SERVER_ERROR, + detail="Cannot get stall products", + ) + + @nostrmarket_ext.delete("/api/v1/stall/{stall_id}") async def api_delete_stall( stall_id: str, wallet: WalletTypeInfo = Depends(require_admin_key) @@ -310,6 +329,11 @@ async def api_create_product( data.validate_product() product = await create_product(wallet.wallet.user, data=data) + event = await sign_and_send_to_nostr(wallet.wallet.user, product) + + product.config.event_id = event.id + await update_product(wallet.wallet.user, product) + return product except ValueError as ex: raise HTTPException( @@ -334,6 +358,11 @@ async def api_update_product( product.validate_product() product = await update_product(wallet.wallet.user, product) + event = await sign_and_send_to_nostr(wallet.wallet.user, product) + + product.config.event_id = event.id + await update_product(wallet.wallet.user, product) + return product except ValueError as ex: raise HTTPException( @@ -348,13 +377,13 @@ async def api_update_product( ) -@nostrmarket_ext.get("/api/v1/product/{stall_id}") +@nostrmarket_ext.get("/api/v1/product/{product_id}") async def api_get_product( - stall_id: str, + product_id: str, wallet: WalletTypeInfo = Depends(require_invoice_key), -): +) -> Optional[Product]: try: - products = await get_products(wallet.wallet.user, stall_id) + products = await get_product(wallet.wallet.user, product_id) return products except Exception as ex: logger.warning(ex) @@ -370,7 +399,18 @@ async def api_delete_product( wallet: WalletTypeInfo = Depends(require_admin_key), ): try: + product = await get_product(wallet.wallet.user, 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) + + except HTTPException as ex: + raise ex except Exception as ex: logger.warning(ex) raise HTTPException( @@ -385,3 +425,23 @@ async def api_delete_product( @nostrmarket_ext.get("/api/v1/currencies") async def api_list_currencies_available(): return list(currencies.keys()) + + +######################################## 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 From 1622403cd187bc66f73fd5e11141dcbfd2e91e32 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Fri, 3 Mar 2023 11:22:48 +0200 Subject: [PATCH 2/5] refactor: stalls use `sign_and_send_to_nostr` --- crud.py | 6 +++--- views_api.py | 30 ++++++++---------------------- 2 files changed, 11 insertions(+), 25 deletions(-) diff --git a/crud.py b/crud.py index f4d85c5..6ad0139 100644 --- a/crud.py +++ b/crud.py @@ -222,7 +222,7 @@ async def update_product(user_id: str, product: Product) -> Product: await db.execute( f""" - UPDATE nostrmarket.products set name = ?, description = ?, images = ?, price = ?, quantity = ?, category_list = ?, meta = ? + UPDATE nostrmarket.products set name = ?, images = ?, price = ?, quantity = ?, category_list = ?, meta = ? WHERE user_id = ? AND id = ? """, ( @@ -230,10 +230,10 @@ async def update_product(user_id: str, product: Product) -> Product: product.image, product.price, product.quantity, + json.dumps(product.categories), + json.dumps(product.config.dict()), user_id, product.id, - json.dumps(product.categories), - json.dumps(product.config), ), ) updated_product = await get_product(user_id, product.id) diff --git a/views_api.py b/views_api.py index 63965d9..e095ee8 100644 --- a/views_api.py +++ b/views_api.py @@ -172,15 +172,9 @@ async def api_create_stall( try: data.validate_stall() - print("### stall", json.dumps(data.dict())) - merchant = await get_merchant_for_user(wallet.wallet.user) - assert merchant, "Cannot find merchat for stall" - stall = await create_stall(wallet.wallet.user, data=data) - event = stall.to_nostr_event(merchant.public_key) - event.sig = merchant.sign_hash(bytes.fromhex(event.id)) - await publish_nostr_event(event) + event = await sign_and_send_to_nostr(wallet.wallet.user, stall) stall.config.event_id = event.id await update_stall(wallet.wallet.user, stall) @@ -207,18 +201,13 @@ async def api_update_stall( try: data.validate_stall() - merchant = await get_merchant_for_user(wallet.wallet.user) - assert merchant, "Cannot find merchat for stall" - - event = data.to_nostr_event(merchant.public_key) - event.sig = merchant.sign_hash(bytes.fromhex(event.id)) - - data.config.event_id = event.id - # data.config.event_created_at = stall = await update_stall(wallet.wallet.user, data) assert stall, "Cannot update stall" - await publish_nostr_event(event) + event = await sign_and_send_to_nostr(wallet.wallet.user, stall) + + stall.config.event_id = event.id + await update_stall(wallet.wallet.user, stall) return stall except HTTPException as ex: @@ -297,15 +286,12 @@ async def api_delete_stall( detail="Stall does not exist.", ) - merchant = await get_merchant_for_user(wallet.wallet.user) - assert merchant, "Cannot find merchat for stall" - await delete_stall(wallet.wallet.user, stall_id) - delete_event = stall.to_nostr_delete_event(merchant.public_key) - delete_event.sig = merchant.sign_hash(bytes.fromhex(delete_event.id)) + event = await sign_and_send_to_nostr(wallet.wallet.user, stall, True) - await publish_nostr_event(delete_event) + stall.config.event_id = event.id + await update_stall(wallet.wallet.user, stall) except HTTPException as ex: raise ex From dccd781553fb85d42d57c0882185fb300b6a5257 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Fri, 3 Mar 2023 11:23:09 +0200 Subject: [PATCH 3/5] feat: show stall ID --- models.py | 2 +- static/components/stall-details/stall-details.html | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/models.py b/models.py index 0becfe3..cc82538 100644 --- a/models.py +++ b/models.py @@ -90,7 +90,7 @@ class PartialStall(BaseModel): ) -class Stall(PartialStall): +class Stall(PartialStall, Nostrable): id: str def to_nostr_event(self, pubkey: str) -> NostrEvent: diff --git a/static/components/stall-details/stall-details.html b/static/components/stall-details/stall-details.html index 9246aad..2488726 100644 --- a/static/components/stall-details/stall-details.html +++ b/static/components/stall-details/stall-details.html @@ -7,6 +7,20 @@
+
+
ID:
+
+ +
+
+
Name:
From 39f79fbda545d65051f2c8fd65fc8fa573a077b8 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Fri, 3 Mar 2023 11:30:48 +0200 Subject: [PATCH 4/5] feat: add currency to product --- models.py | 2 ++ nostr/nostr_client.py | 2 +- views_api.py | 13 +++++++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/models.py b/models.py index cc82538..194cf04 100644 --- a/models.py +++ b/models.py @@ -137,6 +137,7 @@ class Stall(PartialStall, Nostrable): class ProductConfig(BaseModel): event_id: Optional[str] description: Optional[str] + currency: Optional[str] class PartialProduct(BaseModel): @@ -177,6 +178,7 @@ class Product(PartialProduct, Nostrable): "name": self.name, "description": self.config.description, "image": self.image, + "currency": self.config.currency, "price": self.price, "quantity": self.quantity, } diff --git a/nostr/nostr_client.py b/nostr/nostr_client.py index 29a5b42..306c123 100644 --- a/nostr/nostr_client.py +++ b/nostr/nostr_client.py @@ -10,7 +10,7 @@ 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)) + # print("### published", dict(data)) async with httpx.AsyncClient() as client: try: await client.post( diff --git a/views_api.py b/views_api.py index e095ee8..4680977 100644 --- a/views_api.py +++ b/views_api.py @@ -313,6 +313,11 @@ async def api_create_product( ) -> Product: try: data.validate_product() + + stall = await get_stall(wallet.wallet.user, data.stall_id) + assert stall, "Stall missing for product" + data.config.currency = stall.currency + product = await create_product(wallet.wallet.user, data=data) event = await sign_and_send_to_nostr(wallet.wallet.user, product) @@ -341,7 +346,15 @@ async def api_update_product( wallet: WalletTypeInfo = Depends(require_admin_key), ) -> Product: try: + if product_id != product.id: + raise ValueError("Bad product ID") + product.validate_product() + + stall = await get_stall(wallet.wallet.user, product.stall_id) + assert stall, "Stall missing for product" + product.config.currency = stall.currency + product = await update_product(wallet.wallet.user, product) event = await sign_and_send_to_nostr(wallet.wallet.user, product) From 6b3cde90a3c9c7e8fdc8b8cdde97a627f670eb40 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Fri, 3 Mar 2023 11:38:11 +0200 Subject: [PATCH 5/5] fix: product description --- .../components/stall-details/stall-details.html | 2 +- static/components/stall-details/stall-details.js | 16 ++++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/static/components/stall-details/stall-details.html b/static/components/stall-details/stall-details.html index 2488726..b26caf3 100644 --- a/static/components/stall-details/stall-details.html +++ b/static/components/stall-details/stall-details.html @@ -201,7 +201,7 @@