feat: create order on DM

This commit is contained in:
Vlad Stan 2023-03-06 15:03:02 +02:00
parent cec7d2ee25
commit d0471744e0
8 changed files with 246 additions and 71 deletions

View file

@ -1,5 +1,5 @@
import asyncio import asyncio
from asyncio import Task from asyncio import Queue, Task
from typing import List from typing import List
from fastapi import APIRouter from fastapi import APIRouter
@ -26,16 +26,29 @@ def nostrmarket_renderer():
return template_renderer(["lnbits/extensions/nostrmarket/templates"]) return template_renderer(["lnbits/extensions/nostrmarket/templates"])
recieve_event_queue: Queue = Queue()
send_req_queue: Queue = Queue()
scheduled_tasks: List[Task] = [] scheduled_tasks: List[Task] = []
from .tasks import subscribe_nostrclient, wait_for_nostr_events, wait_for_paid_invoices
from .tasks import (
subscribe_to_nostr_client,
wait_for_nostr_events,
wait_for_paid_invoices,
)
from .views import * # noqa from .views import * # noqa
from .views_api import * # noqa from .views_api import * # noqa
def nostrmarket_start(): def nostrmarket_start():
async def _subscribe_to_nostr_client():
await subscribe_to_nostr_client(recieve_event_queue, send_req_queue)
async def _wait_for_nostr_events():
await wait_for_nostr_events(recieve_event_queue, send_req_queue)
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
task1 = loop.create_task(catch_everything_and_restart(wait_for_paid_invoices)) task1 = loop.create_task(catch_everything_and_restart(wait_for_paid_invoices))
task2 = loop.create_task(catch_everything_and_restart(subscribe_nostrclient)) task2 = loop.create_task(catch_everything_and_restart(_subscribe_to_nostr_client))
task3 = loop.create_task(catch_everything_and_restart(wait_for_nostr_events)) task3 = loop.create_task(catch_everything_and_restart(_wait_for_nostr_events))
scheduled_tasks.append([task1, task2, task3]) scheduled_tasks.append([task1, task2, task3])

63
crud.py
View file

@ -7,7 +7,9 @@ from lnbits.helpers import urlsafe_short_hash
from . import db from . import db
from .models import ( from .models import (
Merchant, Merchant,
Order,
PartialMerchant, PartialMerchant,
PartialOrder,
PartialProduct, PartialProduct,
PartialStall, PartialStall,
PartialZone, PartialZone,
@ -206,7 +208,7 @@ async def delete_stall(user_id: str, stall_id: str) -> None:
) )
######################################## STALL ######################################## ######################################## PRODUCTS ########################################
async def create_product(user_id: str, data: PartialProduct) -> Product: async def create_product(user_id: str, data: PartialProduct) -> Product:
@ -214,7 +216,7 @@ async def create_product(user_id: str, data: PartialProduct) -> Product:
await db.execute( await db.execute(
f""" f"""
INSERT INTO nostrmarket.products (user_id, id, stall_id, name, images, price, quantity, category_list, meta) INSERT INTO nostrmarket.products (user_id, id, stall_id, name, image, price, quantity, category_list, meta)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""", """,
( (
@ -278,6 +280,29 @@ async def get_products(user_id: str, stall_id: str) -> List[Product]:
return [Product.from_row(row) for row in rows] return [Product.from_row(row) for row in rows]
async def get_products_by_ids(user_id: str, product_ids: List[str]) -> List[Product]:
q = ",".join(["?"] * len(product_ids))
rows = await db.fetchall(
f"SELECT id, stall_id, name, price, quantity FROM nostrmarket.products WHERE user_id = ? AND id IN ({q})",
(user_id, *product_ids),
)
return [Product.from_row(row) for row in rows]
async def get_wallet_for_product(product_id: str) -> Optional[str]:
row = await db.fetchone(
"""
SELECT s.wallet FROM nostrmarket.products p
INNER JOIN nostrmarket.stalls s
ON p.stall_id = s.id
WHERE p.id=?
""",
(product_id,),
)
return row[0] if row else None
async def delete_product(user_id: str, product_id: str) -> None: async def delete_product(user_id: str, product_id: str) -> None:
await db.execute( await db.execute(
"DELETE FROM nostrmarket.products WHERE user_id =? AND id = ?", "DELETE FROM nostrmarket.products WHERE user_id =? AND id = ?",
@ -286,3 +311,37 @@ async def delete_product(user_id: str, product_id: str) -> None:
product_id, product_id,
), ),
) )
######################################## ORDERS ########################################
async def create_order(user_id: str, o: Order) -> Order:
await db.execute(
f"""
INSERT INTO nostrmarket.orders (user_id, id, event_id, pubkey, contact_data, order_items, invoice_id, total)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""",
(
user_id,
o.id,
o.event_id,
o.pubkey,
json.dumps(o.contact.dict()),
json.dumps([i.dict() for i in o.items]),
o.invoice_id,
o.total,
),
)
order = await get_order(user_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]:
row = await db.fetchone(
"SELECT * FROM nostrmarket.orders WHERE user_id =? AND id = ?",
(
user_id,
order_id,
),
)
return Order.from_row(row) if row else None

View file

@ -1,7 +1,7 @@
import base64 import base64
import json import json
import secrets import secrets
from typing import Optional from typing import Any, Optional, Tuple
import secp256k1 import secp256k1
from cffi import FFI from cffi import FFI
@ -73,9 +73,9 @@ def copy_x(output, x32, y32, data):
return 1 return 1
def is_json(string: str): def order_from_json(s: str) -> Tuple[Optional[Any], Optional[str]]:
try: try:
json.loads(string) order = json.loads(s)
except ValueError as e: return (order, None) if "items" in order else (None, s)
return False except ValueError:
return True return None, s

View file

@ -71,39 +71,25 @@ async def m001_initial(db):
""" """
Initial orders table. Initial orders table.
""" """
empty_object = "{}"
await db.execute( await db.execute(
f""" f"""
CREATE TABLE nostrmarket.orders ( CREATE TABLE nostrmarket.orders (
user_id TEXT NOT NULL,
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
wallet TEXT NOT NULL, event_id TEXT,
username TEXT,
pubkey TEXT, pubkey TEXT,
shipping_zone TEXT NOT NULL, contact_data TEXT NOT NULL DEFAULT '{empty_object}',
address TEXT, order_items TEXT NOT NULL,
email TEXT,
total REAL NOT NULL, total REAL NOT NULL,
invoice_id TEXT NOT NULL, invoice_id TEXT NOT NULL,
paid BOOLEAN NOT NULL, paid BOOLEAN NOT NULL DEFAULT false,
shipped BOOLEAN NOT NULL, shipped BOOLEAN NOT NULL DEFAULT false,
time TIMESTAMP NOT NULL DEFAULT {db.timestamp_now} time TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
); );
""" """
) )
"""
Initial order details table.
"""
await db.execute(
f"""
CREATE TABLE nostrmarket.order_details (
id TEXT PRIMARY KEY,
order_id TEXT NOT NULL,
product_id TEXT NOT NULL,
quantity INTEGER NOT NULL
);
"""
)
""" """
Initial market table. Initial market table.
""" """
@ -117,19 +103,6 @@ async def m001_initial(db):
""" """
) )
"""
Initial market stalls table.
"""
await db.execute(
f"""
CREATE TABLE nostrmarket.market_stalls (
id TEXT PRIMARY KEY,
market_id TEXT NOT NULL,
stall_id TEXT NOT NULL
);
"""
)
""" """
Initial chat messages table. Initial chat messages table.
""" """

View file

@ -217,3 +217,40 @@ class Product(PartialProduct, Nostrable):
product.config = ProductConfig(**json.loads(row["meta"])) product.config = ProductConfig(**json.loads(row["meta"]))
product.categories = json.loads(row["category_list"]) product.categories = json.loads(row["category_list"])
return product return product
######################################## ORDERS ########################################
class OrderItem(BaseModel):
product_id: str
quantity: int
class OrderContact(BaseModel):
nostr: Optional[str]
phone: Optional[str]
email: Optional[str]
class PartialOrder(BaseModel):
id: Optional[str]
event_id: Optional[str]
pubkey: str
items: List[OrderItem]
contact: Optional[OrderContact]
class Order(PartialOrder):
id: str
invoice_id: str
total: float
paid: bool = False
shipped: bool = False
@classmethod
def from_row(cls, row: Row) -> "Order":
contact = OrderContact(**json.loads(row["contact_data"]))
items = [OrderItem(**z) for z in json.loads(row["order_items"])]
order = cls(**dict(row), contact=contact, items=items)
return order

View file

@ -46,21 +46,21 @@ async def connect_to_nostrclient_ws(
return ws return ws
async def handle_event(event, pubkeys): # async def handle_event(event, pubkeys):
tags = [t[1] for t in event["tags"] if t[0] == "p"] # tags = [t[1] for t in event["tags"] if t[0] == "p"]
to_merchant = None # to_merchant = None
if tags and len(tags) > 0: # if tags and len(tags) > 0:
to_merchant = tags[0] # to_merchant = tags[0]
if event["pubkey"] in pubkeys or to_merchant in pubkeys: # if event["pubkey"] in pubkeys or to_merchant in pubkeys:
logger.debug(f"Event sent to {to_merchant}") # logger.debug(f"Event sent to {to_merchant}")
pubkey = to_merchant if to_merchant in pubkeys else event["pubkey"] # pubkey = to_merchant if to_merchant in pubkeys else event["pubkey"]
# Send event to market extension # # Send event to market extension
await send_event_to_market(event=event, pubkey=pubkey) # await send_event_to_market(event=event, pubkey=pubkey)
async def send_event_to_market(event: dict, pubkey: str): # async def send_event_to_market(event: dict, pubkey: str):
# Sends event to market extension, for decrypt and handling # # Sends event to market extension, for decrypt and handling
market_url = url_for(f"/market/api/v1/nip04/{pubkey}", external=True) # market_url = url_for(f"/market/api/v1/nip04/{pubkey}", external=True)
async with httpx.AsyncClient() as client: # async with httpx.AsyncClient() as client:
await client.post(url=market_url, json=event) # await client.post(url=market_url, json=event)

View file

@ -7,16 +7,22 @@ import websocket
from loguru import logger from loguru import logger
from websocket import WebSocketApp from websocket import WebSocketApp
from lnbits.core import get_wallet
from lnbits.core.models import Payment from lnbits.core.models import Payment
from lnbits.extensions.nostrmarket.models import PartialOrder
from lnbits.helpers import url_for
from lnbits.tasks import register_invoice_listener from lnbits.tasks import register_invoice_listener
from .crud import get_merchant, get_merchant_by_pubkey, get_public_keys_for_merchants from .crud import (
get_merchant_by_pubkey,
get_product,
get_public_keys_for_merchants,
get_wallet_for_product,
)
from .helpers import order_from_json
from .nostr.event import NostrEvent from .nostr.event import NostrEvent
from .nostr.nostr_client import connect_to_nostrclient_ws from .nostr.nostr_client import connect_to_nostrclient_ws
recieve_event_queue: Queue = Queue()
send_req_queue: Queue = Queue()
async def wait_for_paid_invoices(): async def wait_for_paid_invoices():
invoice_queue = Queue() invoice_queue = Queue()
@ -34,7 +40,7 @@ async def on_invoice_paid(payment: Payment) -> None:
print("### on_invoice_paid") print("### on_invoice_paid")
async def subscribe_nostrclient(): async def subscribe_to_nostr_client(recieve_event_queue: Queue, send_req_queue: Queue):
print("### subscribe_nostrclient_ws") print("### subscribe_nostrclient_ws")
def on_open(_): def on_open(_):
@ -65,7 +71,7 @@ async def subscribe_nostrclient():
await asyncio.sleep(5) await asyncio.sleep(5)
async def wait_for_nostr_events(): async def wait_for_nostr_events(recieve_event_queue: Queue, send_req_queue: Queue):
public_keys = await get_public_keys_for_merchants() public_keys = await get_public_keys_for_merchants()
for p in public_keys: for p in public_keys:
await send_req_queue.put( await send_req_queue.put(
@ -85,10 +91,47 @@ async def handle_message(msg: str):
event = NostrEvent(**event) event = NostrEvent(**event)
if event.kind == 4: if event.kind == 4:
merchant = await get_merchant_by_pubkey(public_key) merchant = await get_merchant_by_pubkey(public_key)
if not merchant: assert merchant, f"Merchant not found for public key '{public_key}'"
return
clear_text_msg = merchant.decrypt_message(event.content, event.pubkey) clear_text_msg = merchant.decrypt_message(event.content, event.pubkey)
print("### clear_text_msg", clear_text_msg) await handle_nip04_message(
event.pubkey, event.id, clear_text_msg
)
except Exception as ex: except Exception as ex:
logger.warning(ex) logger.warning(ex)
async def handle_nip04_message(from_pubkey: str, event_id: str, msg: str):
order, text_msg = order_from_json(msg)
try:
if order:
print("### order", from_pubkey, event_id, msg)
### check that event_id not parsed already
order["pubkey"] = from_pubkey
order["event_id"] = event_id
partial_order = PartialOrder(**order)
assert len(partial_order.items) != 0, "Order has no items. Order: " + msg
first_product_id = partial_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:
await client.post(
url=market_url,
headers={
"X-Api-Key": wallet.adminkey,
},
json=order,
)
else:
print("### text_msg", text_msg)
except Exception as ex:
logger.warning(ex)

View file

@ -5,6 +5,7 @@ from typing import List, Optional
from fastapi import Depends from fastapi import Depends
from fastapi.exceptions import HTTPException from fastapi.exceptions import HTTPException
from loguru import logger from loguru import logger
from lnbits.core import create_invoice
from lnbits.decorators import ( from lnbits.decorators import (
WalletTypeInfo, WalletTypeInfo,
@ -17,6 +18,7 @@ from lnbits.utils.exchange_rates import currencies
from . import nostrmarket_ext from . import nostrmarket_ext
from .crud import ( from .crud import (
create_merchant, create_merchant,
create_order,
create_product, create_product,
create_stall, create_stall,
create_zone, create_zone,
@ -26,8 +28,10 @@ from .crud import (
get_merchant_for_user, get_merchant_for_user,
get_product, get_product,
get_products, get_products,
get_products_by_ids,
get_stall, get_stall,
get_stalls, get_stalls,
get_wallet_for_product,
get_zone, get_zone,
get_zones, get_zones,
update_product, update_product,
@ -37,7 +41,9 @@ from .crud import (
from .models import ( from .models import (
Merchant, Merchant,
Nostrable, Nostrable,
Order,
PartialMerchant, PartialMerchant,
PartialOrder,
PartialProduct, PartialProduct,
PartialStall, PartialStall,
PartialZone, PartialZone,
@ -101,7 +107,7 @@ async def api_get_zones(wallet: WalletTypeInfo = Depends(get_key_type)) -> List[
@nostrmarket_ext.post("/api/v1/zone") @nostrmarket_ext.post("/api/v1/zone")
async def api_create_zone( async def api_create_zone(
data: PartialZone, wallet: WalletTypeInfo = Depends(get_key_type) data: PartialZone, wallet: WalletTypeInfo = Depends(require_admin_key)
): ):
try: try:
zone = await create_zone(wallet.wallet.user, data) zone = await create_zone(wallet.wallet.user, data)
@ -418,6 +424,50 @@ 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)
):
try:
data.id = data.id or data.event_id
wallet_id = await get_wallet_for_product(data.items[0].product_id)
assert wallet_id, "Missing wallet for order `{data.id}`"
product_ids = [p.product_id for p in data.items]
products = await get_products_by_ids(wallet.wallet.user, product_ids)
product_prices = {}
for p in products:
product_prices[p.id] = p
amount: float = 0 # todo
for item in data.items:
amount += item.quantity * product_prices[item.product_id].price
payment_hash, payment_request = await create_invoice(
wallet_id=wallet_id,
amount=round(amount),
memo=f"Order '{data.id}' for pubkey '{data.pubkey}'",
extra={
"tag": "nostrmarket",
"order_id": data.id,
}
)
order = Order(**data.dict(), invoice_id=payment_hash, total=100)
await create_order(wallet.wallet.user, order)
except Exception as ex:
logger.warning(ex)
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail="Cannot create order",
)
######################################## OTHER ######################################## ######################################## OTHER ########################################