From 192ef1770eb108a3a64e64b8452682ba329718e1 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Tue, 7 Mar 2023 15:10:18 +0200 Subject: [PATCH 01/19] feat: save incoming DMs --- crud.py | 41 +++++++++++++++++++++++++++++++++++++++++ migrations.py | 27 ++++++++------------------- models.py | 20 ++++++++++++++++++++ tasks.py | 16 +++++++++++++--- views_api.py | 1 + 5 files changed, 83 insertions(+), 22 deletions(-) diff --git a/crud.py b/crud.py index 2e6013b..8a5f474 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, @@ -405,3 +407,42 @@ async def update_order_shipped_status( (order_id,), ) return Order.from_row(row) if row else None + + +######################################## 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, message, public_key, incomming) + VALUES (?, ?, ?, ?, ?, ?) + """, + (merchant_id, dm_id, dm.event_id, dm.message, dm.public_key, dm.incomming), + ) + + 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_messages(merchant_id: str, public_key: str) -> List[DirectMessage]: + rows = await db.fetchall( + "SELECT * FROM nostrmarket.zones WHERE merchant_id = ? AND public_ley = ?", + (merchant_id, public_key), + ) + return [DirectMessage.from_row(row) for row in rows] diff --git a/migrations.py b/migrations.py index d6c3ffe..08105fa 100644 --- a/migrations.py +++ b/migrations.py @@ -93,31 +93,20 @@ async def m001_initial(db): """ ) - """ - Initial market table. - """ - await db.execute( - """ - CREATE TABLE nostrmarket.markets ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL, - name TEXT - ); - """ - ) - """ Initial chat messages table. """ 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, + message TEXT NOT NULL, + public_key TEXT NOT NULL, + incomming BOOLEAN NOT NULL DEFAULT false, + time TIMESTAMP NOT NULL DEFAULT {db.timestamp_now} + ); """ ) diff --git a/models.py b/models.py index f228ed5..47220a9 100644 --- a/models.py +++ b/models.py @@ -359,3 +359,23 @@ class PaymentRequest(BaseModel): id: str message: Optional[str] payment_options: List[PaymentOption] + + +######################################## MESSAGE ######################################## + + +class PartialDirectMessage(BaseModel): + event_id: Optional[str] + message: str + public_key: str + incomming: bool = False + time: Optional[int] + + +class DirectMessage(BaseModel): + id: str + + @classmethod + def from_row(cls, row: Row) -> "DirectMessage": + dm = cls(**dict(row)) + return dm diff --git a/tasks.py b/tasks.py index c97c2b2..278e72a 100644 --- a/tasks.py +++ b/tasks.py @@ -13,13 +13,14 @@ from lnbits.helpers import Optional, url_for from lnbits.tasks import register_invoice_listener from .crud import ( + create_direct_message, get_merchant_by_pubkey, 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 .models import OrderStatusUpdate, PartialDirectMessage, PartialOrder from .nostr.event import NostrEvent from .nostr.nostr_client import connect_to_nostrclient_ws, publish_nostr_event @@ -125,14 +126,16 @@ async def handle_nip04_message(public_key: str, event: NostrEvent): 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) + dm_content = await handle_dirrect_message( + merchant.id, 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 + merchant_id: str, from_pubkey: str, event_id: str, msg: str ) -> Optional[str]: order, text_msg = order_from_json(msg) try: @@ -142,6 +145,13 @@ async def handle_dirrect_message( return await handle_new_order(PartialOrder(**order)) else: print("### text_msg", text_msg) + dm = PartialDirectMessage( + event_id=event_id, + message=text_msg, + public_key=from_pubkey, + incomming=True, + ) + await create_direct_message(merchant_id, dm) return None except Exception as ex: logger.warning(ex) diff --git a/views_api.py b/views_api.py index 6d561d8..8dc1810 100644 --- a/views_api.py +++ b/views_api.py @@ -594,6 +594,7 @@ async def api_stop(wallet: WalletTypeInfo = Depends(check_admin)): return {"success": True} + ######################################## HELPERS ######################################## From b7c099c9355932cea5e97061099dcd1cc2f5893d Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Tue, 7 Mar 2023 15:23:59 +0200 Subject: [PATCH 02/19] feat: add empty DMs box --- .../direct-messages/direct-messages.html | 1 + .../direct-messages/direct-messages.js | 14 ++++++++++ static/js/index.js | 1 + templates/nostrmarket/_api_docs.html | 2 -- templates/nostrmarket/index.html | 28 +++++++++++-------- 5 files changed, 33 insertions(+), 13 deletions(-) create mode 100644 static/components/direct-messages/direct-messages.html create mode 100644 static/components/direct-messages/direct-messages.js diff --git a/static/components/direct-messages/direct-messages.html b/static/components/direct-messages/direct-messages.html new file mode 100644 index 0000000..159da03 --- /dev/null +++ b/static/components/direct-messages/direct-messages.html @@ -0,0 +1 @@ +
dmmms
diff --git a/static/components/direct-messages/direct-messages.js b/static/components/direct-messages/direct-messages.js new file mode 100644 index 0000000..02f3fd9 --- /dev/null +++ b/static/components/direct-messages/direct-messages.js @@ -0,0 +1,14 @@ +async function directMessages(path) { + const template = await loadTemplateAsync(path) + Vue.component('direct-messages', { + name: 'direct-messages', + props: ['adminkey', 'inkey'], + template, + + data: function () { + return {} + }, + methods: {}, + created: async function () {} + }) +} diff --git a/static/js/index.js b/static/js/index.js index 3eb50ad..cfb214c 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -6,6 +6,7 @@ const merchant = async () => { await stallDetails('static/components/stall-details/stall-details.html') await stallList('static/components/stall-list/stall-list.html') await orderList('static/components/order-list/order-list.html') + await directMessages('static/components/direct-messages/direct-messages.html') const nostr = window.NostrTools 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 @@ >

-
-
- - -
- {{SITE_TITLE}} Nostr Market Extension -
-
- - - {% include "nostrmarket/_api_docs.html" %} - -
+ +
+ + +
+ {{SITE_TITLE}} Nostr Market Extension +
+
+ + + {% include "nostrmarket/_api_docs.html" %} + +
+
+
x111
+
@@ -151,6 +156,7 @@ + {% endblock %} From 240da5277f461a76a77d4638e9f9cf6ced77d506 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Tue, 7 Mar 2023 18:17:46 +0200 Subject: [PATCH 03/19] feat: basic chat --- crud.py | 6 +- migrations.py | 2 +- models.py | 4 +- .../direct-messages/direct-messages.html | 59 ++++++++++++++++++- .../direct-messages/direct-messages.js | 48 ++++++++++++++- tasks.py | 2 +- templates/nostrmarket/index.html | 47 ++++++++++++++- views_api.py | 47 +++++++++++++++ 8 files changed, 202 insertions(+), 13 deletions(-) diff --git a/crud.py b/crud.py index 8a5f474..be4d627 100644 --- a/crud.py +++ b/crud.py @@ -418,10 +418,10 @@ async def create_direct_message( dm_id = urlsafe_short_hash() await db.execute( f""" - INSERT INTO nostrmarket.direct_messages (merchant_id, id, event_id, message, public_key, incomming) + INSERT INTO nostrmarket.direct_messages (merchant_id, id, event_id, message, public_key, incoming) VALUES (?, ?, ?, ?, ?, ?) """, - (merchant_id, dm_id, dm.event_id, dm.message, dm.public_key, dm.incomming), + (merchant_id, dm_id, dm.event_id, dm.message, dm.public_key, dm.incoming), ) msg = await get_direct_message(merchant_id, dm_id) @@ -442,7 +442,7 @@ async def get_direct_message(merchant_id: str, dm_id: str) -> Optional[DirectMes async def get_direct_messages(merchant_id: str, public_key: str) -> List[DirectMessage]: rows = await db.fetchall( - "SELECT * FROM nostrmarket.zones WHERE merchant_id = ? AND public_ley = ?", + "SELECT * FROM nostrmarket.direct_messages WHERE merchant_id = ? AND public_key = ? ORDER BY time DESC", (merchant_id, public_key), ) return [DirectMessage.from_row(row) for row in rows] diff --git a/migrations.py b/migrations.py index 08105fa..e9ec647 100644 --- a/migrations.py +++ b/migrations.py @@ -104,7 +104,7 @@ async def m001_initial(db): event_id TEXT, message TEXT NOT NULL, public_key TEXT NOT NULL, - incomming BOOLEAN NOT NULL DEFAULT false, + incoming BOOLEAN NOT NULL DEFAULT false, time TIMESTAMP NOT NULL DEFAULT {db.timestamp_now} ); """ diff --git a/models.py b/models.py index 47220a9..09d753a 100644 --- a/models.py +++ b/models.py @@ -368,11 +368,11 @@ class PartialDirectMessage(BaseModel): event_id: Optional[str] message: str public_key: str - incomming: bool = False + incoming: bool = False time: Optional[int] -class DirectMessage(BaseModel): +class DirectMessage(PartialDirectMessage): id: str @classmethod diff --git a/static/components/direct-messages/direct-messages.html b/static/components/direct-messages/direct-messages.html index 159da03..de7665e 100644 --- a/static/components/direct-messages/direct-messages.html +++ b/static/components/direct-messages/direct-messages.html @@ -1 +1,58 @@ -
dmmms
+
+ + +
Messages
+
+ + + + + + + +
+
+
+ +
+
+ + + + + + + +
+
+
+
diff --git a/static/components/direct-messages/direct-messages.js b/static/components/direct-messages/direct-messages.js index 02f3fd9..4e14431 100644 --- a/static/components/direct-messages/direct-messages.js +++ b/static/components/direct-messages/direct-messages.js @@ -6,9 +6,51 @@ async function directMessages(path) { template, data: function () { - return {} + return { + activePublicKey: + '83d07a79496f4cbdc50ca585741a79a2df1fd938cfa449f0fccb0ab7352115dd', + messages: [], + newMessage: '' + } }, - methods: {}, - created: async function () {} + 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) + } 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.push(data) + this.newMessage = '' + } catch (error) { + LNbits.utils.notifyApiError(error) + } + }, + }, + created: async function () { + await this.getDirectMessages() + } }) } diff --git a/tasks.py b/tasks.py index 278e72a..c9eb535 100644 --- a/tasks.py +++ b/tasks.py @@ -149,7 +149,7 @@ async def handle_dirrect_message( event_id=event_id, message=text_msg, public_key=from_pubkey, - incomming=True, + incoming=True, ) await create_direct_message(merchant_id, dm) return None diff --git a/templates/nostrmarket/index.html b/templates/nostrmarket/index.html index a6fa53d..ec96692 100644 --- a/templates/nostrmarket/index.html +++ b/templates/nostrmarket/index.html @@ -101,7 +101,6 @@
-
@@ -115,7 +114,13 @@
-
x111
+
+ + +
@@ -146,6 +151,43 @@
{% endblock%}{% block scripts %} {{ window_vars(user) }} + + + + @@ -159,4 +201,5 @@ + {% endblock %} diff --git a/views_api.py b/views_api.py index 8dc1810..6434332 100644 --- a/views_api.py +++ b/views_api.py @@ -18,6 +18,7 @@ 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, @@ -26,6 +27,7 @@ from .crud import ( delete_product, delete_stall, delete_zone, + get_direct_messages, get_merchant_for_user, get_order, get_order_by_event_id, @@ -45,11 +47,13 @@ from .crud import ( update_zone, ) from .models import ( + DirectMessage, Merchant, Nostrable, Order, OrderExtra, OrderStatusUpdate, + PartialDirectMessage, PartialMerchant, PartialOrder, PartialProduct, @@ -576,6 +580,49 @@ 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 zone", + ) + +@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 + + 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 ######################################## From ef6f2ebe337779201dd14e1445dffe41265c710e Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Wed, 8 Mar 2023 09:52:01 +0200 Subject: [PATCH 04/19] feat: store `event_created_at` for some events --- crud.py | 21 +++++++++++----- migrations.py | 2 ++ models.py | 2 ++ .../direct-messages/direct-messages.js | 14 +++++++---- tasks.py | 8 +++--- templates/nostrmarket/index.html | 25 ++----------------- views_api.py | 2 ++ 7 files changed, 37 insertions(+), 37 deletions(-) diff --git a/crud.py b/crud.py index be4d627..2d89175 100644 --- a/crud.py +++ b/crud.py @@ -318,13 +318,14 @@ async def delete_product(user_id: str, product_id: str) -> None: async def create_order(user_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 (user_id, id, event_id, event_created_at, pubkey, address, contact_data, extra_data, order_items, stall_id, invoice_id, total) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( user_id, o.id, o.event_id, + o.event_created_at, o.pubkey, o.address, json.dumps(o.contact.dict() if o.contact else {}), @@ -418,10 +419,18 @@ async def create_direct_message( dm_id = urlsafe_short_hash() await db.execute( f""" - INSERT INTO nostrmarket.direct_messages (merchant_id, id, event_id, message, public_key, incoming) - VALUES (?, ?, ?, ?, ?, ?) + INSERT INTO nostrmarket.direct_messages (merchant_id, id, event_id, event_created_at, message, public_key, incoming) + VALUES (?, ?, ?, ?, ?, ?, ?) """, - (merchant_id, dm_id, dm.event_id, dm.message, dm.public_key, dm.incoming), + ( + merchant_id, + dm_id, + dm.event_id, + dm.event_created_at, + dm.message, + dm.public_key, + dm.incoming, + ), ) msg = await get_direct_message(merchant_id, dm_id) @@ -442,7 +451,7 @@ async def get_direct_message(merchant_id: str, dm_id: str) -> Optional[DirectMes 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 time DESC", + "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] diff --git a/migrations.py b/migrations.py index e9ec647..6b3c3c5 100644 --- a/migrations.py +++ b/migrations.py @@ -78,6 +78,7 @@ async def m001_initial(db): user_id TEXT NOT NULL, id TEXT PRIMARY KEY, event_id TEXT, + event_created_at INTEGER NOT NULL, pubkey TEXT NOT NULL, contact_data TEXT NOT NULL DEFAULT '{empty_object}', extra_data TEXT NOT NULL DEFAULT '{empty_object}', @@ -102,6 +103,7 @@ async def m001_initial(db): merchant_id TEXT NOT NULL, id TEXT PRIMARY KEY, event_id TEXT, + event_created_at INTEGER NOT NULL, message TEXT NOT NULL, public_key TEXT NOT NULL, incoming BOOLEAN NOT NULL DEFAULT false, diff --git a/models.py b/models.py index 09d753a..c76d25b 100644 --- a/models.py +++ b/models.py @@ -281,6 +281,7 @@ class OrderExtra(BaseModel): class PartialOrder(BaseModel): id: str event_id: Optional[str] + event_created_at: Optional[int] pubkey: str items: List[OrderItem] contact: Optional[OrderContact] @@ -366,6 +367,7 @@ class PaymentRequest(BaseModel): class PartialDirectMessage(BaseModel): event_id: Optional[str] + event_created_at: Optional[int] message: str public_key: str incoming: bool = False diff --git a/static/components/direct-messages/direct-messages.js b/static/components/direct-messages/direct-messages.js index 4e14431..7a4299a 100644 --- a/static/components/direct-messages/direct-messages.js +++ b/static/components/direct-messages/direct-messages.js @@ -26,7 +26,10 @@ async function directMessages(path) { this.inkey ) this.messages = data - console.log('### this.messages', this.messages) + console.log( + '### this.messages', + this.messages.map(m => m.message) + ) } catch (error) { LNbits.utils.notifyApiError(error) } @@ -38,16 +41,17 @@ async function directMessages(path) { '/nostrmarket/api/v1/message', this.adminkey, { - message: this.newMessage, - public_key: this.activePublicKey + message: this.newMessage, + public_key: this.activePublicKey } ) - this.messages.push(data) + this.messages = this.messages.concat([data]) + console.log('### this.messages', this.messages) this.newMessage = '' } catch (error) { LNbits.utils.notifyApiError(error) } - }, + } }, created: async function () { await this.getDirectMessages() diff --git a/tasks.py b/tasks.py index c9eb535..06e6a87 100644 --- a/tasks.py +++ b/tasks.py @@ -127,7 +127,7 @@ async def handle_nip04_message(public_key: str, event: NostrEvent): clear_text_msg = merchant.decrypt_message(event.content, event.pubkey) dm_content = await handle_dirrect_message( - merchant.id, event.pubkey, event.id, clear_text_msg + merchant.id, event.pubkey, event.id, event.created_at, clear_text_msg ) if dm_content: dm_event = merchant.build_dm_event(dm_content, event.pubkey) @@ -135,18 +135,20 @@ async def handle_nip04_message(public_key: str, event: NostrEvent): async def handle_dirrect_message( - merchant_id: str, from_pubkey: str, event_id: str, msg: str + merchant_id: 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["pubkey"] = from_pubkey 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) dm = PartialDirectMessage( event_id=event_id, + event_created_at=event_created_at, message=text_msg, public_key=from_pubkey, incoming=True, @@ -158,7 +160,7 @@ async def handle_dirrect_message( return None -async def handle_new_order(order: PartialOrder): +async def handle_new_order(order: PartialOrder) -> Optional[str]: ### todo: check that event_id not parsed already order.validate_order() diff --git a/templates/nostrmarket/index.html b/templates/nostrmarket/index.html index ec96692..fe7c6b0 100644 --- a/templates/nostrmarket/index.html +++ b/templates/nostrmarket/index.html @@ -114,7 +114,7 @@ -
+
{% endblock%}{% block scripts %} {{ window_vars(user) }} - @@ -201,5 +181,4 @@ - {% endblock %} diff --git a/views_api.py b/views_api.py index 6434332..d3cca1e 100644 --- a/views_api.py +++ b/views_api.py @@ -600,6 +600,7 @@ async def api_get_messages( detail="Cannot get zone", ) + @nostrmarket_ext.post("/api/v1/message") async def api_create_message( data: PartialDirectMessage, wallet: WalletTypeInfo = Depends(require_admin_key) @@ -610,6 +611,7 @@ async def api_create_message( 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) From bfea056747ac1f06d25938e94973d37971e04542 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Wed, 8 Mar 2023 13:31:52 +0200 Subject: [PATCH 05/19] feat: base for getting out messages --- tasks.py | 33 ++++++++++++++++++++++++++++---- templates/nostrmarket/index.html | 2 +- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/tasks.py b/tasks.py index 06e6a87..8123a8b 100644 --- a/tasks.py +++ b/tasks.py @@ -100,8 +100,11 @@ async def wait_for_nostr_events(recieve_event_queue: Queue, send_req_queue: Queu public_keys = await get_public_keys_for_merchants() for p in public_keys: await send_req_queue.put( - ["REQ", f"direct-messages:{p}", {"kind": 4, "#p": [p]}] + ["REQ", f"direct-messages-in:{p}", {"kind": 4, "#p": [p]}] ) + # await send_req_queue.put( + # ["REQ", f"direct-messages-out:{p}", {"kind": 4, "authors": [p]}] + # ) while True: message = await recieve_event_queue.get() @@ -111,21 +114,30 @@ async def wait_for_nostr_events(recieve_event_queue: Queue, send_req_queue: Queu async def handle_message(msg: str): try: type, subscription_id, event = json.loads(msg) - _, public_key = subscription_id.split(":") + subscription_name, public_key = subscription_id.split(":") if type.upper() == "EVENT": event = NostrEvent(**event) if event.kind == 4: - await handle_nip04_message(public_key, event) + await handle_nip04_message(subscription_name, public_key, event) except Exception as ex: logger.warning(ex) -async def handle_nip04_message(public_key: str, event: NostrEvent): +async def handle_nip04_message( + subscription_name: str, 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) + 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, merchant, clear_text_msg): dm_content = await handle_dirrect_message( merchant.id, event.pubkey, event.id, event.created_at, clear_text_msg ) @@ -134,6 +146,19 @@ async def handle_nip04_message(public_key: str, event: NostrEvent): await publish_nostr_event(dm_event) +async def handle_outgoing_dms(event, merchant, clear_text_msg): + 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, from_pubkey: str, event_id: str, event_created_at: int, msg: str ) -> Optional[str]: diff --git a/templates/nostrmarket/index.html b/templates/nostrmarket/index.html index fe7c6b0..89fc385 100644 --- a/templates/nostrmarket/index.html +++ b/templates/nostrmarket/index.html @@ -114,7 +114,7 @@
-
+
Date: Wed, 8 Mar 2023 14:28:43 +0200 Subject: [PATCH 06/19] fix: direct message focus --- .../components/direct-messages/direct-messages.html | 4 +++- static/components/direct-messages/direct-messages.js | 11 +++++++++++ templates/nostrmarket/index.html | 1 + 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/static/components/direct-messages/direct-messages.html b/static/components/direct-messages/direct-messages.html index de7665e..7845939 100644 --- a/static/components/direct-messages/direct-messages.html +++ b/static/components/direct-messages/direct-messages.html @@ -18,14 +18,16 @@
-
+
diff --git a/static/components/direct-messages/direct-messages.js b/static/components/direct-messages/direct-messages.js index 7a4299a..93846bc 100644 --- a/static/components/direct-messages/direct-messages.js +++ b/static/components/direct-messages/direct-messages.js @@ -30,6 +30,8 @@ async function directMessages(path) { '### this.messages', this.messages.map(m => m.message) ) + this.focusOnChatBox(this.messages.length - 1) + } catch (error) { LNbits.utils.notifyApiError(error) } @@ -48,9 +50,18 @@ async function directMessages(path) { 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 () { diff --git a/templates/nostrmarket/index.html b/templates/nostrmarket/index.html index 89fc385..fa0d29d 100644 --- a/templates/nostrmarket/index.html +++ b/templates/nostrmarket/index.html @@ -166,6 +166,7 @@ margin-left: auto; width: 100%; } + From 82a04cc8b9cd6d875988d2839b8404149a7c65e5 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Wed, 8 Mar 2023 14:42:23 +0200 Subject: [PATCH 07/19] chore: code format --- static/components/direct-messages/direct-messages.html | 3 +-- static/components/direct-messages/direct-messages.js | 9 +++++---- templates/nostrmarket/index.html | 1 - 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/static/components/direct-messages/direct-messages.html b/static/components/direct-messages/direct-messages.html index 7845939..17cf401 100644 --- a/static/components/direct-messages/direct-messages.html +++ b/static/components/direct-messages/direct-messages.html @@ -18,11 +18,10 @@
-
+
m.message) ) this.focusOnChatBox(this.messages.length - 1) - } catch (error) { LNbits.utils.notifyApiError(error) } @@ -50,14 +49,16 @@ async function directMessages(path) { this.messages = this.messages.concat([data]) console.log('### this.messages', this.messages) this.newMessage = '' - this.focusOnChatBox(this.messages.length - 1) + this.focusOnChatBox(this.messages.length - 1) } catch (error) { LNbits.utils.notifyApiError(error) } }, - focusOnChatBox: function(index) { + focusOnChatBox: function (index) { setTimeout(() => { - const lastChatBox = document.getElementsByClassName(`chat-mesage-index-${index}`); + const lastChatBox = document.getElementsByClassName( + `chat-mesage-index-${index}` + ) if (lastChatBox && lastChatBox[0]) { lastChatBox[0].scrollIntoView() } diff --git a/templates/nostrmarket/index.html b/templates/nostrmarket/index.html index fa0d29d..89fc385 100644 --- a/templates/nostrmarket/index.html +++ b/templates/nostrmarket/index.html @@ -166,7 +166,6 @@ margin-left: auto; width: 100%; } - From c0c737378b03b2d2ed94ba2104ba110fd5c10f93 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Wed, 8 Mar 2023 14:42:43 +0200 Subject: [PATCH 08/19] refactor: extract `create_order` --- services.py | 80 ++++++++++++++++++++++++++++++++++++++++++++++++++++ views_api.py | 74 ++---------------------------------------------- 2 files changed, 82 insertions(+), 72 deletions(-) create mode 100644 services.py diff --git a/services.py b/services.py new file mode 100644 index 0000000..83f064e --- /dev/null +++ b/services.py @@ -0,0 +1,80 @@ +from typing import Optional + +from lnbits.core import create_invoice + +from .crud import ( + get_merchant_for_user, + get_order, + get_order_by_event_id, + get_products_by_ids, + get_wallet_for_product, +) +from .models import ( + Nostrable, + Order, + OrderExtra, + PartialOrder, + PaymentOption, + PaymentRequest, +) +from .nostr.event import NostrEvent +from .nostr.nostr_client import publish_nostr_event + + +async def create_order(user_id: str, data: PartialOrder) -> Optional[PaymentRequest]: + if await get_order(user_id, data.id): + return None + if data.event_id and await get_order_by_event_id(user_id, data.event_id): + return None + + merchant = await get_merchant_for_user(user_id) + assert merchant, "Cannot find merchant!" + + products = await get_products_by_ids(user_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.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(user_id, order) + + return PaymentRequest( + id=data.id, payment_options=[PaymentOption(type="ln", link=invoice)] + ) + + +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 diff --git a/views_api.py b/views_api.py index d3cca1e..f1fd193 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, @@ -20,7 +19,6 @@ from . import nostrmarket_ext, scheduled_tasks from .crud import ( create_direct_message, create_merchant, - create_order, create_product, create_stall, create_zone, @@ -30,15 +28,12 @@ from .crud import ( 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, @@ -49,9 +44,7 @@ from .crud import ( from .models import ( DirectMessage, Merchant, - Nostrable, Order, - OrderExtra, OrderStatusUpdate, PartialDirectMessage, PartialMerchant, @@ -59,14 +52,13 @@ from .models import ( PartialProduct, PartialStall, PartialZone, - PaymentOption, PaymentRequest, Product, Stall, Zone, ) -from .nostr.event import NostrEvent from .nostr.nostr_client import publish_nostr_event +from .services import create_order ######################################## MERCHANT ######################################## @@ -463,49 +455,7 @@ async def api_create_order( ) -> 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)] - ) + return await create_order(wallet.wallet.user, data) except Exception as ex: logger.warning(ex) raise HTTPException( @@ -642,23 +592,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 From 6c6cd861ced0bcf1901be559a54fb594c90292d0 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Wed, 8 Mar 2023 14:45:32 +0200 Subject: [PATCH 09/19] refactor: extract `handle_order_paid` --- services.py | 25 +++++++++++++++++++++++++ tasks.py | 23 +++-------------------- 2 files changed, 28 insertions(+), 20 deletions(-) diff --git a/services.py b/services.py index 83f064e..ca32d40 100644 --- a/services.py +++ b/services.py @@ -1,18 +1,23 @@ +import json from typing import Optional +from loguru import logger from lnbits.core import create_invoice from .crud import ( + get_merchant_by_pubkey, get_merchant_for_user, get_order, get_order_by_event_id, get_products_by_ids, get_wallet_for_product, + update_order_paid_status, ) from .models import ( Nostrable, Order, OrderExtra, + OrderStatusUpdate, PartialOrder, PaymentOption, PaymentRequest, @@ -78,3 +83,23 @@ async def sign_and_send_to_nostr( 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.pubkey) + await publish_nostr_event(dm_event) + except Exception as ex: + logger.warning(ex) \ No newline at end of file diff --git a/tasks.py b/tasks.py index 8123a8b..1b961f6 100644 --- a/tasks.py +++ b/tasks.py @@ -3,6 +3,7 @@ import json from asyncio import Queue import httpx + import websocket from loguru import logger from websocket import WebSocketApp @@ -17,13 +18,12 @@ from .crud import ( get_merchant_by_pubkey, get_public_keys_for_merchants, get_wallet_for_product, - update_order_paid_status, ) from .helpers import order_from_json -from .models import OrderStatusUpdate, PartialDirectMessage, PartialOrder +from .models import PartialDirectMessage, PartialOrder from .nostr.event import NostrEvent from .nostr.nostr_client import connect_to_nostrclient_ws, publish_nostr_event - +from .services import handle_order_paid async def wait_for_paid_invoices(): invoice_queue = Queue() @@ -46,24 +46,7 @@ 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): From 7f3438d07fcb995801cfb121376c2015d894d094 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Wed, 8 Mar 2023 15:17:24 +0200 Subject: [PATCH 10/19] refactor: clean-up `tasks.py` --- services.py | 122 +++++++++++++++++++++++++++++++++++++++++++++++-- tasks.py | 127 ++------------------------------------------------- views_api.py | 4 +- 3 files changed, 126 insertions(+), 127 deletions(-) diff --git a/services.py b/services.py index ca32d40..b499900 100644 --- a/services.py +++ b/services.py @@ -1,10 +1,14 @@ import json from typing import Optional + +import httpx from loguru import logger -from lnbits.core import create_invoice +from lnbits.core import create_invoice, get_wallet, url_for from .crud import ( + create_direct_message, + create_order, get_merchant_by_pubkey, get_merchant_for_user, get_order, @@ -13,11 +17,14 @@ from .crud import ( 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, @@ -26,7 +33,9 @@ from .nostr.event import NostrEvent from .nostr.nostr_client import publish_nostr_event -async def create_order(user_id: str, data: PartialOrder) -> Optional[PaymentRequest]: +async def create_new_order( + user_id: str, data: PartialOrder +) -> Optional[PaymentRequest]: if await get_order(user_id, data.id): return None if data.event_id and await get_order_by_event_id(user_id, data.event_id): @@ -102,4 +111,111 @@ async def handle_order_paid(order_id: str, merchant_pubkey: str): dm_event = merchant.build_dm_event(dm_content, order.pubkey) await publish_nostr_event(dm_event) except Exception as ex: - logger.warning(ex) \ No newline at end of file + logger.warning(ex) + + +async def process_nostr_message(msg: str): + try: + type, subscription_id, event = json.loads(msg) + subscription_name, public_key = subscription_id.split(":") + if type.upper() == "EVENT": + event = NostrEvent(**event) + if event.kind == 4: + await _handle_nip04_message(subscription_name, public_key, event) + + except Exception as ex: + logger.warning(ex) + + +async def _handle_nip04_message( + subscription_name: str, 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) + 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, 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, 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["pubkey"] = from_pubkey + 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) + 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}" + + 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 diff --git a/tasks.py b/tasks.py index 1b961f6..c1a20ba 100644 --- a/tasks.py +++ b/tasks.py @@ -2,28 +2,17 @@ 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 ( - create_direct_message, - get_merchant_by_pubkey, - get_public_keys_for_merchants, - get_wallet_for_product, -) -from .helpers import order_from_json -from .models import PartialDirectMessage, PartialOrder -from .nostr.event import NostrEvent -from .nostr.nostr_client import connect_to_nostrclient_ws, publish_nostr_event -from .services import handle_order_paid +from .crud import get_public_keys_for_merchants +from .nostr.nostr_client import connect_to_nostrclient_ws +from .services import handle_order_paid, process_nostr_message + async def wait_for_paid_invoices(): invoice_queue = Queue() @@ -46,9 +35,6 @@ async def on_invoice_paid(payment: Payment) -> None: await handle_order_paid(order_id, merchant_pubkey) - - - async def subscribe_to_nostr_client(recieve_event_queue: Queue, send_req_queue: Queue): print("### subscribe_nostrclient_ws") @@ -91,107 +77,4 @@ async def wait_for_nostr_events(recieve_event_queue: Queue, send_req_queue: Queu 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) - subscription_name, public_key = subscription_id.split(":") - if type.upper() == "EVENT": - event = NostrEvent(**event) - if event.kind == 4: - await handle_nip04_message(subscription_name, public_key, event) - - except Exception as ex: - logger.warning(ex) - - -async def handle_nip04_message( - subscription_name: str, 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) - 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, merchant, clear_text_msg): - dm_content = await handle_dirrect_message( - merchant.id, 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, merchant, clear_text_msg): - 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, 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["pubkey"] = from_pubkey - 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) - 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}" - - 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/views_api.py b/views_api.py index f1fd193..05f077f 100644 --- a/views_api.py +++ b/views_api.py @@ -58,7 +58,7 @@ from .models import ( Zone, ) from .nostr.nostr_client import publish_nostr_event -from .services import create_order +from .services import create_new_order, sign_and_send_to_nostr ######################################## MERCHANT ######################################## @@ -455,7 +455,7 @@ async def api_create_order( ) -> Optional[PaymentRequest]: try: # print("### new order: ", json.dumps(data.dict())) - return await create_order(wallet.wallet.user, data) + return await create_new_order(wallet.wallet.user, data) except Exception as ex: logger.warning(ex) raise HTTPException( From bf670c35457e2454e06bf2846b10326ceb1fc11b Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Wed, 8 Mar 2023 15:36:50 +0200 Subject: [PATCH 11/19] refactor: do not go over http to create order --- crud.py | 2 ++ migrations.py | 6 ++++-- services.py | 19 ++++--------------- views_api.py | 19 +------------------ 4 files changed, 11 insertions(+), 35 deletions(-) diff --git a/crud.py b/crud.py index 2d89175..a4d513f 100644 --- a/crud.py +++ b/crud.py @@ -320,6 +320,7 @@ async def create_order(user_id: str, o: Order) -> Order: f""" INSERT INTO nostrmarket.orders (user_id, id, event_id, event_created_at, pubkey, address, contact_data, extra_data, order_items, stall_id, invoice_id, total) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(event_id) DO NOTHING """, ( user_id, @@ -421,6 +422,7 @@ async def create_direct_message( 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, diff --git a/migrations.py b/migrations.py index 6b3c3c5..c52dea3 100644 --- a/migrations.py +++ b/migrations.py @@ -89,7 +89,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} + time TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}, + UNIQUE(event_id) ); """ ) @@ -107,7 +108,8 @@ async def m001_initial(db): message TEXT NOT NULL, public_key TEXT NOT NULL, incoming BOOLEAN NOT NULL DEFAULT false, - time TIMESTAMP NOT NULL DEFAULT {db.timestamp_now} + time TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}, + UNIQUE(event_id) ); """ ) diff --git a/services.py b/services.py index b499900..93de86d 100644 --- a/services.py +++ b/services.py @@ -1,10 +1,9 @@ import json from typing import Optional -import httpx from loguru import logger -from lnbits.core import create_invoice, get_wallet, url_for +from lnbits.core import create_invoice, get_wallet from .crud import ( create_direct_message, @@ -204,18 +203,8 @@ async def _handle_new_order(order: PartialOrder) -> Optional[str]: 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) + 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/views_api.py b/views_api.py index 05f077f..b654e0c 100644 --- a/views_api.py +++ b/views_api.py @@ -48,17 +48,15 @@ from .models import ( OrderStatusUpdate, PartialDirectMessage, PartialMerchant, - PartialOrder, PartialProduct, PartialStall, PartialZone, - PaymentRequest, Product, Stall, Zone, ) from .nostr.nostr_client import publish_nostr_event -from .services import create_new_order, sign_and_send_to_nostr +from .services import sign_and_send_to_nostr ######################################## MERCHANT ######################################## @@ -449,21 +447,6 @@ 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())) - return await create_new_order(wallet.wallet.user, data) - 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}") From 69dcbcb002223916cae0314c16c06acbf51c791b Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Wed, 8 Mar 2023 18:33:23 +0200 Subject: [PATCH 12/19] feat: optimize filtering for DMs --- crud.py | 43 ++++++++++++++++++-- migrations.py | 3 +- models.py | 3 +- services.py | 23 ++++++++--- static/components/order-list/order-list.html | 6 +-- tasks.py | 18 ++++++-- views_api.py | 2 +- 7 files changed, 79 insertions(+), 19 deletions(-) diff --git a/crud.py b/crud.py index a4d513f..5fbad90 100644 --- a/crud.py +++ b/crud.py @@ -318,8 +318,22 @@ async def delete_product(user_id: str, product_id: str) -> None: async def create_order(user_id: str, o: Order) -> Order: await db.execute( f""" - INSERT INTO nostrmarket.orders (user_id, id, event_id, event_created_at, pubkey, address, contact_data, extra_data, order_items, stall_id, invoice_id, total) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + INSERT INTO nostrmarket.orders ( + user_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 """, ( @@ -327,7 +341,8 @@ async def create_order(user_id: str, o: Order) -> Order: o.id, o.event_id, o.event_created_at, - o.pubkey, + o.merchant_public_key, + o.public_key, o.address, json.dumps(o.contact.dict() if o.contact else {}), json.dumps(o.extra.dict()), @@ -384,6 +399,17 @@ async def get_orders_for_stall(user_id: str, stall_id: str) -> List[Order]: 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 = ?", @@ -457,3 +483,14 @@ async def get_direct_messages(merchant_id: str, public_key: str) -> List[DirectM (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 diff --git a/migrations.py b/migrations.py index c52dea3..522fa7b 100644 --- a/migrations.py +++ b/migrations.py @@ -79,7 +79,8 @@ async def m001_initial(db): id TEXT PRIMARY KEY, event_id TEXT, event_created_at INTEGER NOT NULL, - pubkey TEXT 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, diff --git a/models.py b/models.py index c76d25b..379d3f8 100644 --- a/models.py +++ b/models.py @@ -282,7 +282,8 @@ class PartialOrder(BaseModel): id: str event_id: Optional[str] event_created_at: Optional[int] - pubkey: str + public_key: str + merchant_public_key: str items: List[OrderItem] contact: Optional[OrderContact] address: Optional[str] diff --git a/services.py b/services.py index 93de86d..5f11739 100644 --- a/services.py +++ b/services.py @@ -54,7 +54,7 @@ async def create_new_order( payment_hash, invoice = await create_invoice( wallet_id=wallet_id, amount=round(total_amount), - memo=f"Order '{data.id}' for pubkey '{data.pubkey}'", + memo=f"Order '{data.id}' for pubkey '{data.public_key}'", extra={ "tag": "nostrmarket", "order_id": data.id, @@ -107,7 +107,7 @@ async def handle_order_paid(order_id: str, merchant_pubkey: str): order_status.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) except Exception as ex: logger.warning(ex) @@ -143,7 +143,12 @@ async def _handle_incoming_dms( event: NostrEvent, merchant: Merchant, clear_text_msg: str ): dm_content = await _handle_dirrect_message( - merchant.id, event.pubkey, event.id, event.created_at, clear_text_msg + 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) @@ -166,17 +171,23 @@ async def _handle_outgoing_dms( async def _handle_dirrect_message( - merchant_id: str, from_pubkey: str, event_id: str, event_created_at: int, msg: str + 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["pubkey"] = from_pubkey + 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) + print("### text_msg", text_msg, event_created_at, event_id) dm = PartialDirectMessage( event_id=event_id, event_created_at=event_created_at, 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/tasks.py b/tasks.py index c1a20ba..7d4d9c4 100644 --- a/tasks.py +++ b/tasks.py @@ -9,7 +9,11 @@ from websocket import WebSocketApp from lnbits.core.models import Payment from lnbits.tasks import register_invoice_listener -from .crud import get_public_keys_for_merchants +from .crud import ( + get_last_direct_messages_time, + get_last_order_time, + get_public_keys_for_merchants, +) from .nostr.nostr_client import connect_to_nostrclient_ws from .services import handle_order_paid, process_nostr_message @@ -68,9 +72,15 @@ 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: - await send_req_queue.put( - ["REQ", f"direct-messages-in:{p}", {"kind": 4, "#p": [p]}] - ) + 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]} + if since != 0: + in_messages_filter["since"] = since + print("### in_messages_filter", in_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-out:{p}", {"kind": 4, "authors": [p]}] # ) diff --git a/views_api.py b/views_api.py index b654e0c..1f9aaee 100644 --- a/views_api.py +++ b/views_api.py @@ -500,7 +500,7 @@ async def api_update_order_status( 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 From 6c5299fe0583423df0a44a3a25b7d7e7df17d3fb Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Tue, 14 Mar 2023 11:19:32 +0200 Subject: [PATCH 13/19] feat: add merchant-details --- .../merchant-details/merchant-details.html | 37 ++++++++++++++ .../merchant-details/merchant-details.js | 48 +++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 static/components/merchant-details/merchant-details.html create mode 100644 static/components/merchant-details/merchant-details.js 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 () {} + }) +} From 152fe5baabdc21aebe56b3536935b22a5b588f16 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Tue, 14 Mar 2023 11:19:53 +0200 Subject: [PATCH 14/19] feat: add merchant-details --- .../shipping-zones/shipping-zones.html | 1 + static/js/index.js | 6 ++++++ templates/nostrmarket/index.html | 18 ++++++++---------- 3 files changed, 15 insertions(+), 10 deletions(-) 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/js/index.js b/static/js/index.js index cfb214c..435cfd8 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -7,6 +7,9 @@ const merchant = async () => { 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 @@ -52,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/templates/nostrmarket/index.html b/templates/nostrmarket/index.html index 89fc385..641f642 100644 --- a/templates/nostrmarket/index.html +++ b/templates/nostrmarket/index.html @@ -61,6 +61,13 @@
+
+ +
-
- - Show Public and Private keys - -
@@ -179,6 +176,7 @@ + {% endblock %} From 3e0b480f0a573a741001632a6443c66dbaf35eed Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Tue, 14 Mar 2023 12:36:42 +0200 Subject: [PATCH 15/19] feat: user `merchant_id` instead of `user_id` --- __init__.py | 4 + crud.py | 174 +++++++++++-------- helpers.py | 2 + migrations.py | 14 +- nostr/nostr_client.py | 2 +- services.py | 45 ++--- static/components/stall-list/stall-list.html | 2 +- tasks.py | 15 +- templates/nostrmarket/index.html | 78 ++++----- views_api.py | 137 +++++++++++---- 10 files changed, 286 insertions(+), 187 deletions(-) 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 5fbad90..5e56000 100644 --- a/crud.py +++ b/crud.py @@ -76,16 +76,16 @@ async def get_merchant_for_user(user_id: str) -> Optional[Merchant]: ######################################## 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, @@ -93,55 +93,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, @@ -153,35 +165,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, @@ -191,18 +203,18 @@ 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, ), ) @@ -211,16 +223,16 @@ async def delete_stall(user_id: str, stall_id: str) -> None: ######################################## 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, @@ -231,18 +243,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, @@ -251,40 +263,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] @@ -302,11 +316,11 @@ 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, ), ) @@ -315,11 +329,11 @@ async def delete_product(user_id: str, product_id: str) -> None: ######################################## 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, + merchant_id, id, event_id, event_created_at, @@ -337,7 +351,7 @@ async def create_order(user_id: str, o: Order) -> Order: ON CONFLICT(event_id) DO NOTHING """, ( - user_id, + merchant_id, o.id, o.event_id, o.event_created_at, @@ -352,47 +366,47 @@ 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, ), ) @@ -423,11 +437,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( @@ -460,8 +474,10 @@ async def create_direct_message( dm.incoming, ), ) - - msg = await get_direct_message(merchant_id, dm_id) + 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 @@ -476,6 +492,16 @@ async def get_direct_message(merchant_id: str, dm_id: str) -> Optional[DirectMes ) 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( 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 522fa7b..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,7 +75,7 @@ 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, event_created_at INTEGER NOT NULL, @@ -120,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/nostr/nostr_client.py b/nostr/nostr_client.py index 3e8a47e..a05c4ea 100644 --- a/nostr/nostr_client.py +++ b/nostr/nostr_client.py @@ -33,7 +33,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, diff --git a/services.py b/services.py index 5f11739..18ff95e 100644 --- a/services.py +++ b/services.py @@ -33,17 +33,19 @@ from .nostr.nostr_client import publish_nostr_event async def create_new_order( - user_id: str, data: PartialOrder + merchant_public_key: str, data: PartialOrder ) -> Optional[PaymentRequest]: - if await get_order(user_id, data.id): - return None - if data.event_id and await get_order_by_event_id(user_id, data.event_id): - return None - - merchant = await get_merchant_for_user(user_id) + merchant = await get_merchant_by_pubkey(merchant_public_key) assert merchant, "Cannot find merchant!" - products = await get_products_by_ids(user_id, [p.product_id for p in data.items]) + 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) @@ -69,7 +71,7 @@ async def create_new_order( total=total_amount, extra=await OrderExtra.from_products(products), ) - await create_order(user_id, order) + await create_order(merchant.id, order) return PaymentRequest( id=data.id, payment_options=[PaymentOption(type="ln", link=invoice)] @@ -77,11 +79,8 @@ async def create_new_order( async def sign_and_send_to_nostr( - user_id: str, n: Nostrable, delete=False + merchant: Merchant, 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 @@ -115,24 +114,28 @@ async def handle_order_paid(order_id: str, merchant_pubkey: str): async def process_nostr_message(msg: str): try: - type, subscription_id, event = json.loads(msg) - subscription_name, public_key = subscription_id.split(":") + 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, public_key, event) - + 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, public_key: str, event: NostrEvent + subscription_name: str, merchant_public_key: str, event: NostrEvent ): - merchant = await get_merchant_by_pubkey(public_key) - assert merchant, f"Merchant not found for public key '{public_key}'" + 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: @@ -187,7 +190,7 @@ async def _handle_dirrect_message( 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) + # print("### text_msg", text_msg, event_created_at, event_id) dm = PartialDirectMessage( event_id=event_id, event_created_at=event_created_at, 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)
- +
+ + +
+
+ +
+
+
+ +
+
+
+ + + +
+ + + + + +
+ Wellcome to Nostr Market!
In Nostr Market, merchant and customer communicate via NOSTR relays, so @@ -57,44 +95,6 @@
-
- - -
-
- -
-
-
- -
-
-
- - - -
- - - - - -
diff --git a/views_api.py b/views_api.py index 1f9aaee..4027434 100644 --- a/views_api.py +++ b/views_api.py @@ -13,6 +13,7 @@ from lnbits.decorators import ( require_admin_key, require_invoice_key, ) +from lnbits.extensions.nostrmarket.helpers import get_shared_secret from lnbits.utils.exchange_rates import currencies from . import nostrmarket_ext, scheduled_tasks @@ -22,6 +23,7 @@ from .crud import ( create_product, create_stall, create_zone, + delete_merchant_zones, delete_product, delete_stall, delete_zone, @@ -69,6 +71,7 @@ async def api_create_merchant( try: merchant = await create_merchant(wallet.wallet.user, data) + return merchant except Exception as ex: logger.warning(ex) @@ -94,13 +97,33 @@ async def api_get_merchant( ) +@nostrmarket_ext.delete("/api/v1/merchant") +async def api_delete_merchant( + wallet: WalletTypeInfo = Depends(require_admin_key), +): + + try: + merchant = await get_merchant_for_user(wallet.wallet.user) + assert merchant, "Merchant cannot be found" + + await delete_merchant_zones(wallet.wallet.user) + 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( @@ -114,7 +137,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) @@ -131,7 +156,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, @@ -153,7 +180,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( @@ -161,7 +190,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) @@ -182,12 +211,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: @@ -211,13 +242,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: @@ -238,7 +272,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, @@ -258,7 +294,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) @@ -274,7 +312,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) @@ -290,7 +330,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) @@ -305,19 +347,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 @@ -339,17 +383,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: @@ -376,17 +422,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: @@ -408,7 +456,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) @@ -424,15 +475,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 @@ -452,7 +506,10 @@ 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, @@ -472,7 +529,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) @@ -489,12 +549,13 @@ 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 @@ -530,7 +591,7 @@ async def api_get_messages( logger.warning(ex) raise HTTPException( status_code=HTTPStatus.INTERNAL_SERVER_ERROR, - detail="Cannot get zone", + detail="Cannot get direct message", ) From 90bbc797348a8ec1da39a00c3ab1f2e9b655dbdb Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Tue, 14 Mar 2023 14:22:04 +0200 Subject: [PATCH 16/19] chore: code clean-up --- services.py | 3 +-- views_api.py | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/services.py b/services.py index 18ff95e..1e481ec 100644 --- a/services.py +++ b/services.py @@ -9,7 +9,6 @@ from .crud import ( create_direct_message, create_order, get_merchant_by_pubkey, - get_merchant_for_user, get_order, get_order_by_event_id, get_products_by_ids, @@ -114,7 +113,7 @@ async def handle_order_paid(order_id: str, merchant_pubkey: str): async def process_nostr_message(msg: str): try: - type, *rest= json.loads(msg) + type, *rest = json.loads(msg) if type.upper() == "EVENT": subscription_id, event = rest subscription_name, merchant_public_key = subscription_id.split(":") diff --git a/views_api.py b/views_api.py index 4027434..09a22d5 100644 --- a/views_api.py +++ b/views_api.py @@ -13,7 +13,6 @@ from lnbits.decorators import ( require_admin_key, require_invoice_key, ) -from lnbits.extensions.nostrmarket.helpers import get_shared_secret from lnbits.utils.exchange_rates import currencies from . import nostrmarket_ext, scheduled_tasks From f978d9e97f4bf6d04788847434c4cba813519bfa Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Tue, 14 Mar 2023 14:27:13 +0200 Subject: [PATCH 17/19] chore: code format --- crud.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crud.py b/crud.py index 5e56000..747156c 100644 --- a/crud.py +++ b/crud.py @@ -492,7 +492,10 @@ async def get_direct_message(merchant_id: str, dm_id: str) -> Optional[DirectMes ) 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]: + +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 = ?", ( From 9931a085660c6b1bf211ecab227edf74fb7a8959 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Tue, 14 Mar 2023 16:00:01 +0200 Subject: [PATCH 18/19] fix: publish events via websocket --- nostr/nostr_client.py | 36 ++----------------- .../customer-stall/customer-stall.js | 5 +-- tasks.py | 3 -- 3 files changed, 6 insertions(+), 38 deletions(-) diff --git a/nostr/nostr_client.py b/nostr/nostr_client.py index a05c4ea..ec0af68 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( @@ -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/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/tasks.py b/tasks.py index 3919c73..57cc36f 100644 --- a/tasks.py +++ b/tasks.py @@ -40,8 +40,6 @@ async def on_invoice_paid(payment: Payment) -> None: 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") @@ -68,7 +66,6 @@ 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): - print("### wait_for_nostr_events") public_keys = await get_public_keys_for_merchants() for p in public_keys: last_order_time = await get_last_order_time(p) From 92c083399145971d30d4a9adf80a41ef47b3125d Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Tue, 14 Mar 2023 16:26:04 +0200 Subject: [PATCH 19/19] feat: delete merchant from the local DB --- crud.py | 37 +++++++++++++++++++++++++++++++- nostr/nostr_client.py | 2 +- templates/nostrmarket/index.html | 1 + views_api.py | 16 ++++++++++++-- 4 files changed, 52 insertions(+), 4 deletions(-) diff --git a/crud.py b/crud.py index 747156c..f3b7fa6 100644 --- a/crud.py +++ b/crud.py @@ -73,6 +73,13 @@ 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 ######################################## @@ -220,6 +227,13 @@ async def delete_stall(merchant_id: str, stall_id: str) -> None: ) +async def delete_merchant_stalls(merchant_id: str) -> None: + await db.execute( + "DELETE FROM nostrmarket.stalls WHERE merchant_id = ?", + (merchant_id,), + ) + + ######################################## PRODUCTS ######################################## @@ -326,6 +340,13 @@ async def delete_product(merchant_id: str, product_id: str) -> None: ) +async def delete_merchant_products(merchant_id: str) -> None: + await db.execute( + "DELETE FROM nostrmarket.products WHERE merchant_id = ?", + (merchant_id,), + ) + + ######################################## ORDERS ######################################## @@ -451,6 +472,13 @@ async def update_order_shipped_status( 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 @@ -520,6 +548,13 @@ async def get_last_direct_messages_time(public_key: str) -> int: SELECT event_created_at FROM nostrmarket.direct_messages WHERE public_key = ? ORDER BY event_created_at DESC LIMIT 1 """, - (public_key), + (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/nostr/nostr_client.py b/nostr/nostr_client.py index ec0af68..d5bfd7a 100644 --- a/nostr/nostr_client.py +++ b/nostr/nostr_client.py @@ -11,7 +11,7 @@ from .event import NostrEvent async def publish_nostr_event(e: NostrEvent): - print('### publish_nostr_event', e.dict()) + print("### publish_nostr_event", e.dict()) await send_req_queue.put(["EVENT", e.dict()]) diff --git a/templates/nostrmarket/index.html b/templates/nostrmarket/index.html index ecfad1b..32d0ca0 100644 --- a/templates/nostrmarket/index.html +++ b/templates/nostrmarket/index.html @@ -8,6 +8,7 @@