nostrmarket/views_api.py
2023-03-07 13:10:01 +02:00

590 lines
17 KiB
Python

import json
from http import HTTPStatus
from typing import List, Optional
from fastapi import Depends
from fastapi.exceptions import HTTPException
from loguru import logger
from lnbits.core import create_invoice
from lnbits.decorators import (
WalletTypeInfo,
get_key_type,
require_admin_key,
require_invoice_key,
)
from lnbits.utils.exchange_rates import currencies
from . import nostrmarket_ext
from .crud import (
create_merchant,
create_order,
create_product,
create_stall,
create_zone,
delete_product,
delete_stall,
delete_zone,
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_product,
update_stall,
update_zone,
)
from .models import (
Merchant,
Nostrable,
Order,
OrderExtra,
PartialMerchant,
PartialOrder,
PartialProduct,
PartialStall,
PartialZone,
PaymentOption,
PaymentRequest,
Product,
Stall,
Zone,
)
from .nostr.event import NostrEvent
from .nostr.nostr_client import publish_nostr_event
######################################## MERCHANT ########################################
@nostrmarket_ext.post("/api/v1/merchant")
async def api_create_merchant(
data: PartialMerchant,
wallet: WalletTypeInfo = Depends(require_admin_key),
) -> Merchant:
try:
merchant = await create_merchant(wallet.wallet.user, data)
return merchant
except Exception as ex:
logger.warning(ex)
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail="Cannot create merchant",
)
@nostrmarket_ext.get("/api/v1/merchant")
async def api_get_merchant(
wallet: WalletTypeInfo = Depends(require_invoice_key),
) -> Optional[Merchant]:
try:
merchant = await get_merchant_for_user(wallet.wallet.user)
return merchant
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)
except Exception as ex:
logger.warning(ex)
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail="Cannot get zone",
)
@nostrmarket_ext.post("/api/v1/zone")
async def api_create_zone(
data: PartialZone, wallet: WalletTypeInfo = Depends(require_admin_key)
):
try:
zone = await create_zone(wallet.wallet.user, data)
return zone
except Exception as ex:
logger.warning(ex)
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail="Cannot create zone",
)
@nostrmarket_ext.patch("/api/v1/zone/{zone_id}")
async def api_update_zone(
data: Zone,
zone_id: str,
wallet: WalletTypeInfo = Depends(require_admin_key),
) -> Zone:
try:
zone = await get_zone(wallet.wallet.user, zone_id)
if not zone:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND,
detail="Zone does not exist.",
)
zone = await update_zone(wallet.wallet.user, data)
assert zone, "Cannot find updated zone"
return zone
except HTTPException as ex:
raise ex
except Exception as ex:
logger.warning(ex)
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail="Cannot 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)
if not zone:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND,
detail="Zone does not exist.",
)
await delete_zone(zone_id)
except Exception as ex:
logger.warning(ex)
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail="Cannot delete zone",
)
######################################## STALLS ########################################
@nostrmarket_ext.post("/api/v1/stall")
async def api_create_stall(
data: PartialStall,
wallet: WalletTypeInfo = Depends(require_admin_key),
) -> Stall:
try:
data.validate_stall()
stall = await create_stall(wallet.wallet.user, data=data)
event = await sign_and_send_to_nostr(wallet.wallet.user, stall)
stall.config.event_id = event.id
await update_stall(wallet.wallet.user, stall)
return stall
except ValueError as ex:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=str(ex),
)
except Exception as ex:
logger.warning(ex)
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail="Cannot create stall",
)
@nostrmarket_ext.put("/api/v1/stall/{stall_id}")
async def api_update_stall(
data: Stall,
wallet: WalletTypeInfo = Depends(require_admin_key),
) -> Stall:
try:
data.validate_stall()
stall = await update_stall(wallet.wallet.user, data)
assert stall, "Cannot update stall"
event = await sign_and_send_to_nostr(wallet.wallet.user, stall)
stall.config.event_id = event.id
await update_stall(wallet.wallet.user, stall)
return stall
except HTTPException as ex:
raise ex
except ValueError as ex:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=str(ex),
)
except Exception as ex:
logger.warning(ex)
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail="Cannot 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)
if not stall:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND,
detail="Stall does not exist.",
)
return stall
except HTTPException as ex:
raise ex
except Exception as ex:
logger.warning(ex)
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail="Cannot get stall",
)
@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)
return stalls
except Exception as ex:
logger.warning(ex)
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail="Cannot get stalls",
)
@nostrmarket_ext.get("/api/v1/stall/product/{stall_id}")
async def api_get_stall_products(
stall_id: str,
wallet: WalletTypeInfo = Depends(require_invoice_key),
):
try:
products = await get_products(wallet.wallet.user, stall_id)
return products
except Exception as ex:
logger.warning(ex)
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail="Cannot get stall products",
)
@nostrmarket_ext.get("/api/v1/stall/order/{stall_id}")
async def api_get_stall_orders(
stall_id: str,
wallet: WalletTypeInfo = Depends(require_invoice_key),
):
try:
orders = await get_orders_for_stall(wallet.wallet.user, stall_id)
return orders
except Exception as ex:
logger.warning(ex)
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail="Cannot get stall products",
)
@nostrmarket_ext.delete("/api/v1/stall/{stall_id}")
async def api_delete_stall(
stall_id: str, wallet: WalletTypeInfo = Depends(require_admin_key)
):
try:
stall = await get_stall(wallet.wallet.user, 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)
event = await sign_and_send_to_nostr(wallet.wallet.user, stall, True)
stall.config.event_id = event.id
await update_stall(wallet.wallet.user, stall)
except HTTPException as ex:
raise ex
except Exception as ex:
logger.warning(ex)
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail="Cannot delete stall",
)
######################################## PRODUCTS ########################################
@nostrmarket_ext.post("/api/v1/product")
async def api_create_product(
data: PartialProduct,
wallet: WalletTypeInfo = Depends(require_admin_key),
) -> Product:
try:
data.validate_product()
stall = await get_stall(wallet.wallet.user, data.stall_id)
assert stall, "Stall missing for product"
data.config.currency = stall.currency
product = await create_product(wallet.wallet.user, data=data)
event = await sign_and_send_to_nostr(wallet.wallet.user, product)
product.config.event_id = event.id
await update_product(wallet.wallet.user, product)
return product
except ValueError as ex:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=str(ex),
)
except Exception as ex:
logger.warning(ex)
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail="Cannot create product",
)
@nostrmarket_ext.patch("/api/v1/product/{product_id}")
async def api_update_product(
product_id: str,
product: Product,
wallet: WalletTypeInfo = Depends(require_admin_key),
) -> Product:
try:
if product_id != product.id:
raise ValueError("Bad product ID")
product.validate_product()
stall = await get_stall(wallet.wallet.user, product.stall_id)
assert stall, "Stall missing for product"
product.config.currency = stall.currency
product = await update_product(wallet.wallet.user, product)
event = await sign_and_send_to_nostr(wallet.wallet.user, product)
product.config.event_id = event.id
await update_product(wallet.wallet.user, product)
return product
except ValueError as ex:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=str(ex),
)
except Exception as ex:
logger.warning(ex)
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail="Cannot update product",
)
@nostrmarket_ext.get("/api/v1/product/{product_id}")
async def api_get_product(
product_id: str,
wallet: WalletTypeInfo = Depends(require_invoice_key),
) -> Optional[Product]:
try:
products = await get_product(wallet.wallet.user, product_id)
return products
except Exception as ex:
logger.warning(ex)
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail="Cannot get product",
)
@nostrmarket_ext.delete("/api/v1/product/{product_id}")
async def api_delete_product(
product_id: str,
wallet: WalletTypeInfo = Depends(require_admin_key),
):
try:
product = await get_product(wallet.wallet.user, product_id)
if not product:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND,
detail="Product does not exist.",
)
await delete_product(wallet.wallet.user, product_id)
await sign_and_send_to_nostr(wallet.wallet.user, product, True)
except HTTPException as ex:
raise ex
except Exception as ex:
logger.warning(ex)
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail="Cannot delete product",
)
######################################## ORDERS ########################################
@nostrmarket_ext.post("/api/v1/order")
async def api_create_order(
data: PartialOrder, wallet: WalletTypeInfo = Depends(require_admin_key)
) -> Optional[PaymentRequest]:
try:
# print("### new order: ", json.dumps(data.dict()))
if await get_order(wallet.wallet.user, data.id):
return None
if data.event_id and await get_order_by_event_id(
wallet.wallet.user, data.event_id
):
return None
merchant = await get_merchant_for_user(wallet.wallet.user)
assert merchant, "Cannot find merchant!"
products = await get_products_by_ids(
wallet.wallet.user, [p.product_id for p in data.items]
)
data.validate_order_items(products)
total_amount = await data.total_sats(products)
wallet_id = await get_wallet_for_product(data.items[0].product_id)
assert wallet_id, "Missing wallet for order `{data.id}`"
payment_hash, invoice = await create_invoice(
wallet_id=wallet_id,
amount=round(total_amount),
memo=f"Order '{data.id}' for pubkey '{data.pubkey}'",
extra={
"tag": "nostrmarket",
"order_id": data.id,
"merchant_pubkey": merchant.public_key,
},
)
order = Order(
**data.dict(),
stall_id=products[0].stall_id,
invoice_id=payment_hash,
total=total_amount,
extra=await OrderExtra.from_products(products),
)
await create_order(wallet.wallet.user, order)
return PaymentRequest(
id=data.id, payment_options=[PaymentOption(type="ln", link=invoice)]
)
except Exception as ex:
logger.warning(ex)
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail="Cannot create order",
)
nostrmarket_ext.get("/api/v1/order/{order_id}")
async def api_get_order(order_id: str, wallet: WalletTypeInfo = Depends(get_key_type)):
try:
order = await get_order(wallet.wallet.user, order_id)
if not order:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND,
detail="Order does not exist.",
)
return order
except HTTPException as ex:
raise ex
except Exception as ex:
logger.warning(ex)
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail="Cannot get order",
)
@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)
return orders
except Exception as ex:
logger.warning(ex)
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail="Cannot get orders",
)
# @nostrmarket_ext.patch("/api/v1/order/{order_id}")
# async def api_update_order(
# data: OrderStatusUpdate,
# wallet: WalletTypeInfo = Depends(require_admin_key),
# ) -> Zone:
# try:
# zone = await update_order(wallet.wallet.user, data)
# assert zone, "Cannot find updated zone"
# return zone
# except HTTPException as ex:
# raise ex
# except Exception as ex:
# logger.warning(ex)
# raise HTTPException(
# status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
# detail="Cannot update order",
# )
######################################## OTHER ########################################
@nostrmarket_ext.get("/api/v1/currencies")
async def api_list_currencies_available():
return list(currencies.keys())
######################################## HELPERS ########################################
async def sign_and_send_to_nostr(
user_id: str, n: Nostrable, delete=False
) -> NostrEvent:
merchant = await get_merchant_for_user(user_id)
assert merchant, "Cannot find merchant!"
event = (
n.to_nostr_delete_event(merchant.public_key)
if delete
else n.to_nostr_event(merchant.public_key)
)
event.sig = merchant.sign_hash(bytes.fromhex(event.id))
await publish_nostr_event(event)
return event