diff --git a/lnbits/extensions/market/README.md b/lnbits/extensions/market/README.md new file mode 100644 index 00000000..22d38e0d --- /dev/null +++ b/lnbits/extensions/market/README.md @@ -0,0 +1,9 @@ +

Market

+

A movable market stand

+Make a list of products to sell, point the list to an relay (or many), stack sats. +Market is a movable market stand, for anon transactions. You then give permission for an relay to list those products. Delivery addresses are sent through the Lightning Network. + + +

API endpoints

+ +curl -X GET http://YOUR-TOR-ADDRESS diff --git a/lnbits/extensions/market/__init__.py b/lnbits/extensions/market/__init__.py new file mode 100644 index 00000000..3795ec73 --- /dev/null +++ b/lnbits/extensions/market/__init__.py @@ -0,0 +1,43 @@ +import asyncio + +from fastapi import APIRouter +from starlette.staticfiles import StaticFiles + +from lnbits.db import Database +from lnbits.helpers import template_renderer +from lnbits.tasks import catch_everything_and_restart + +db = Database("ext_market") + +market_ext: APIRouter = APIRouter(prefix="/market", tags=["market"]) + +market_static_files = [ + { + "path": "/market/static", + "app": StaticFiles(directory="lnbits/extensions/market/static"), + "name": "market_static", + } +] + +# if 'nostradmin' not in LNBITS_ADMIN_EXTENSIONS: +# @market_ext.get("/", response_class=HTMLResponse) +# async def index(request: Request): +# return template_renderer().TemplateResponse( +# "error.html", {"request": request, "err": "Ask system admin to enable NostrAdmin!"} +# ) +# else: + + +def market_renderer(): + return template_renderer(["lnbits/extensions/market/templates"]) + # return template_renderer(["lnbits/extensions/market/templates"]) + + +from .tasks import wait_for_paid_invoices +from .views import * # noqa +from .views_api import * # noqa + + +def market_start(): + loop = asyncio.get_event_loop() + loop.create_task(catch_everything_and_restart(wait_for_paid_invoices)) diff --git a/lnbits/extensions/market/config.json b/lnbits/extensions/market/config.json new file mode 100644 index 00000000..8a294867 --- /dev/null +++ b/lnbits/extensions/market/config.json @@ -0,0 +1,6 @@ +{ + "name": "Marketplace", + "short_description": "Webshop/market on LNbits", + "tile": "/market/static/images/bitcoin-shop.png", + "contributors": ["benarc", "talvasconcelos"] +} diff --git a/lnbits/extensions/market/crud.py b/lnbits/extensions/market/crud.py new file mode 100644 index 00000000..1d9c28be --- /dev/null +++ b/lnbits/extensions/market/crud.py @@ -0,0 +1,492 @@ +from base64 import urlsafe_b64encode +from typing import List, Optional, Union +from uuid import uuid4 + +# from lnbits.db import open_ext_db +from lnbits.db import SQLITE +from lnbits.helpers import urlsafe_short_hash +from lnbits.settings import WALLET + +from . import db +from .models import ( + ChatMessage, + CreateChatMessage, + CreateMarket, + CreateMarketStalls, + Market, + MarketSettings, + OrderDetail, + Orders, + Products, + Stalls, + Zones, + createOrder, + createOrderDetails, + createProduct, + createStalls, + createZones, +) + +###Products + + +async def create_market_product(data: createProduct) -> Products: + product_id = urlsafe_short_hash() + await db.execute( + f""" + INSERT INTO market.products (id, stall, product, categories, description, image, price, quantity) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + """, + ( + product_id, + data.stall, + data.product, + data.categories, + data.description, + data.image, + data.price, + data.quantity, + ), + ) + product = await get_market_product(product_id) + assert product, "Newly created product couldn't be retrieved" + return product + + +async def update_market_product(product_id: str, **kwargs) -> Optional[Products]: + q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) + + await db.execute( + f"UPDATE market.products SET {q} WHERE id = ?", + (*kwargs.values(), product_id), + ) + row = await db.fetchone("SELECT * FROM market.products WHERE id = ?", (product_id,)) + + return Products(**row) if row else None + + +async def get_market_product(product_id: str) -> Optional[Products]: + row = await db.fetchone("SELECT * FROM market.products WHERE id = ?", (product_id,)) + return Products(**row) if row else None + + +async def get_market_products(stall_ids: Union[str, List[str]]) -> List[Products]: + if isinstance(stall_ids, str): + stall_ids = [stall_ids] + + # with open_ext_db("market") as db: + q = ",".join(["?"] * len(stall_ids)) + rows = await db.fetchall( + f""" + SELECT * FROM market.products WHERE stall IN ({q}) + """, + (*stall_ids,), + ) + return [Products(**row) for row in rows] + + +async def delete_market_product(product_id: str) -> None: + await db.execute("DELETE FROM market.products WHERE id = ?", (product_id,)) + + +###zones + + +async def create_market_zone(user, data: createZones) -> Zones: + zone_id = urlsafe_short_hash() + await db.execute( + f""" + INSERT INTO market.zones ( + id, + "user", + cost, + countries + + ) + VALUES (?, ?, ?, ?) + """, + (zone_id, user, data.cost, data.countries.lower()), + ) + + zone = await get_market_zone(zone_id) + assert zone, "Newly created zone couldn't be retrieved" + return zone + + +async def update_market_zone(zone_id: str, **kwargs) -> Optional[Zones]: + q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) + await db.execute( + f"UPDATE market.zones SET {q} WHERE id = ?", + (*kwargs.values(), zone_id), + ) + row = await db.fetchone("SELECT * FROM market.zones WHERE id = ?", (zone_id,)) + return Zones(**row) if row else None + + +async def get_market_zone(zone_id: str) -> Optional[Zones]: + row = await db.fetchone("SELECT * FROM market.zones WHERE id = ?", (zone_id,)) + return Zones(**row) if row else None + + +async def get_market_zones(user: str) -> List[Zones]: + rows = await db.fetchall('SELECT * FROM market.zones WHERE "user" = ?', (user,)) + return [Zones(**row) for row in rows] + + +async def delete_market_zone(zone_id: str) -> None: + await db.execute("DELETE FROM market.zones WHERE id = ?", (zone_id,)) + + +###Stalls + + +async def create_market_stall(data: createStalls) -> Stalls: + stall_id = urlsafe_short_hash() + await db.execute( + f""" + INSERT INTO market.stalls ( + id, + wallet, + name, + currency, + publickey, + relays, + shippingzones + ) + VALUES (?, ?, ?, ?, ?, ?, ?) + """, + ( + stall_id, + data.wallet, + data.name, + data.currency, + data.publickey, + data.relays, + data.shippingzones, + ), + ) + + stall = await get_market_stall(stall_id) + assert stall, "Newly created stall couldn't be retrieved" + return stall + + +async def update_market_stall(stall_id: str, **kwargs) -> Optional[Stalls]: + q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) + await db.execute( + f"UPDATE market.stalls SET {q} WHERE id = ?", + (*kwargs.values(), stall_id), + ) + row = await db.fetchone("SELECT * FROM market.stalls WHERE id = ?", (stall_id,)) + return Stalls(**row) if row else None + + +async def get_market_stall(stall_id: str) -> Optional[Stalls]: + row = await db.fetchone("SELECT * FROM market.stalls WHERE id = ?", (stall_id,)) + return Stalls(**row) if row else None + + +async def get_market_stalls(wallet_ids: Union[str, List[str]]) -> List[Stalls]: + q = ",".join(["?"] * len(wallet_ids)) + rows = await db.fetchall( + f"SELECT * FROM market.stalls WHERE wallet IN ({q})", (*wallet_ids,) + ) + return [Stalls(**row) for row in rows] + + +async def get_market_stalls_by_ids(stall_ids: Union[str, List[str]]) -> List[Stalls]: + q = ",".join(["?"] * len(stall_ids)) + rows = await db.fetchall( + f"SELECT * FROM market.stalls WHERE id IN ({q})", (*stall_ids,) + ) + return [Stalls(**row) for row in rows] + + +async def delete_market_stall(stall_id: str) -> None: + await db.execute("DELETE FROM market.stalls WHERE id = ?", (stall_id,)) + + +###Orders + + +async def create_market_order(data: createOrder, invoiceid: str): + returning = "" if db.type == SQLITE else "RETURNING ID" + method = db.execute if db.type == SQLITE else db.fetchone + + result = await (method)( + f""" + INSERT INTO market.orders (wallet, shippingzone, address, email, total, invoiceid, paid, shipped) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + {returning} + """, + ( + data.wallet, + data.shippingzone, + data.address, + data.email, + data.total, + invoiceid, + False, + False, + ), + ) + if db.type == SQLITE: + return result._result_proxy.lastrowid + else: + return result[0] + + +async def create_market_order_details(order_id: str, data: List[createOrderDetails]): + for item in data: + item_id = urlsafe_short_hash() + await db.execute( + """ + INSERT INTO market.order_details (id, order_id, product_id, quantity) + VALUES (?, ?, ?, ?) + """, + ( + item_id, + order_id, + item.product_id, + item.quantity, + ), + ) + order_details = await get_market_order_details(order_id) + return order_details + + +async def get_market_order_details(order_id: str) -> List[OrderDetail]: + rows = await db.fetchall( + f"SELECT * FROM market.order_details WHERE order_id = ?", (order_id,) + ) + + return [OrderDetail(**row) for row in rows] + + +async def get_market_order(order_id: str) -> Optional[Orders]: + row = await db.fetchone("SELECT * FROM market.orders WHERE id = ?", (order_id,)) + return Orders(**row) if row else None + + +async def get_market_order_invoiceid(invoice_id: str) -> Optional[Orders]: + row = await db.fetchone( + "SELECT * FROM market.orders WHERE invoiceid = ?", (invoice_id,) + ) + return Orders(**row) if row else None + + +async def set_market_order_paid(payment_hash: str): + await db.execute( + """ + UPDATE market.orders + SET paid = true + WHERE invoiceid = ? + """, + (payment_hash,), + ) + + +async def set_market_order_pubkey(payment_hash: str, pubkey: str): + await db.execute( + """ + UPDATE market.orders + SET pubkey = ? + WHERE invoiceid = ? + """, + ( + pubkey, + payment_hash, + ), + ) + + +async def update_market_product_stock(products): + + q = "\n".join( + [f"""WHEN id='{p.product_id}' THEN quantity - {p.quantity}""" for p in products] + ) + v = ",".join(["?"] * len(products)) + + await db.execute( + f""" + UPDATE market.products + SET quantity=(CASE + {q} + END) + WHERE id IN ({v}); + """, + (*[p.product_id for p in products],), + ) + + +async def get_market_orders(wallet_ids: Union[str, List[str]]) -> List[Orders]: + if isinstance(wallet_ids, str): + wallet_ids = [wallet_ids] + + q = ",".join(["?"] * len(wallet_ids)) + rows = await db.fetchall( + f"SELECT * FROM market.orders WHERE wallet IN ({q})", (*wallet_ids,) + ) + # + return [Orders(**row) for row in rows] + + +async def delete_market_order(order_id: str) -> None: + await db.execute("DELETE FROM market.orders WHERE id = ?", (order_id,)) + + +### Market/Marketplace + + +async def get_market_markets(user: str) -> List[Market]: + rows = await db.fetchall("SELECT * FROM market.markets WHERE usr = ?", (user,)) + return [Market(**row) for row in rows] + + +async def get_market_market(market_id: str) -> Optional[Market]: + row = await db.fetchone("SELECT * FROM market.markets WHERE id = ?", (market_id,)) + return Market(**row) if row else None + + +async def get_market_market_stalls(market_id: str): + rows = await db.fetchall( + "SELECT * FROM market.market_stalls WHERE marketid = ?", (market_id,) + ) + + ids = [row["stallid"] for row in rows] + + return await get_market_stalls_by_ids(ids) + + +async def create_market_market(data: CreateMarket): + market_id = urlsafe_short_hash() + + await db.execute( + """ + INSERT INTO market.markets (id, usr, name) + VALUES (?, ?, ?) + """, + ( + market_id, + data.usr, + data.name, + ), + ) + market = await get_market_market(market_id) + assert market, "Newly created market couldn't be retrieved" + return market + + +async def create_market_market_stalls(market_id: str, data: List[str]): + for stallid in data: + id = urlsafe_short_hash() + + await db.execute( + """ + INSERT INTO market.market_stalls (id, marketid, stallid) + VALUES (?, ?, ?) + """, + ( + id, + market_id, + stallid, + ), + ) + market_stalls = await get_market_market_stalls(market_id) + return market_stalls + + +async def update_market_market(market_id: str, name: str): + await db.execute( + "UPDATE market.markets SET name = ? WHERE id = ?", + (name, market_id), + ) + await db.execute( + "DELETE FROM market.market_stalls WHERE marketid = ?", + (market_id,), + ) + + market = await get_market_market(market_id) + return market + + +### CHAT / MESSAGES + + +async def create_chat_message(data: CreateChatMessage): + await db.execute( + """ + INSERT INTO market.messages (msg, pubkey, id_conversation) + VALUES (?, ?, ?) + """, + ( + data.msg, + data.pubkey, + data.room_name, + ), + ) + + +async def get_market_latest_chat_messages(room_name: str): + rows = await db.fetchall( + "SELECT * FROM market.messages WHERE id_conversation = ? ORDER BY timestamp DESC LIMIT 20", + (room_name,), + ) + + return [ChatMessage(**row) for row in rows] + + +async def get_market_chat_messages(room_name: str): + rows = await db.fetchall( + "SELECT * FROM market.messages WHERE id_conversation = ? ORDER BY timestamp DESC", + (room_name,), + ) + + return [ChatMessage(**row) for row in rows] + + +async def get_market_chat_by_merchant(ids: List[str]) -> List[ChatMessage]: + + q = ",".join(["?"] * len(ids)) + rows = await db.fetchall( + f"SELECT * FROM market.messages WHERE id_conversation IN ({q})", + (*ids,), + ) + return [ChatMessage(**row) for row in rows] + + +async def get_market_settings(user) -> Optional[MarketSettings]: + row = await db.fetchone( + """SELECT * FROM market.settings WHERE "user" = ?""", (user,) + ) + + return MarketSettings(**row) if row else None + + +async def create_market_settings(user: str, data): + await db.execute( + """ + INSERT INTO market.settings ("user", currency, fiat_base_multiplier) + VALUES (?, ?, ?) + """, + ( + user, + data.currency, + data.fiat_base_multiplier, + ), + ) + + +async def set_market_settings(user: str, data): + await db.execute( + """ + UPDATE market.settings + SET currency = ?, fiat_base_multiplier = ? + WHERE "user" = ?; + """, + ( + data.currency, + data.fiat_base_multiplier, + user, + ), + ) diff --git a/lnbits/extensions/market/migrations.py b/lnbits/extensions/market/migrations.py new file mode 100644 index 00000000..72b584f9 --- /dev/null +++ b/lnbits/extensions/market/migrations.py @@ -0,0 +1,156 @@ +async def m001_initial(db): + """ + Initial Market settings table. + """ + await db.execute( + """ + CREATE TABLE market.settings ( + "user" TEXT PRIMARY KEY, + currency TEXT DEFAULT 'sat', + fiat_base_multiplier INTEGER DEFAULT 1 + ); + """ + ) + + """ + Initial stalls table. + """ + await db.execute( + """ + CREATE TABLE market.stalls ( + id TEXT PRIMARY KEY, + wallet TEXT NOT NULL, + name TEXT NOT NULL, + currency TEXT, + publickey TEXT, + relays TEXT, + shippingzones TEXT NOT NULL, + rating INTEGER DEFAULT 0 + ); + """ + ) + + """ + Initial products table. + """ + await db.execute( + f""" + CREATE TABLE market.products ( + id TEXT PRIMARY KEY, + stall TEXT NOT NULL REFERENCES {db.references_schema}stalls (id) ON DELETE CASCADE, + product TEXT NOT NULL, + categories TEXT, + description TEXT, + image TEXT, + price INTEGER NOT NULL, + quantity INTEGER NOT NULL, + rating INTEGER DEFAULT 0 + ); + """ + ) + + """ + Initial zones table. + """ + await db.execute( + """ + CREATE TABLE market.zones ( + id TEXT PRIMARY KEY, + "user" TEXT NOT NULL, + cost TEXT NOT NULL, + countries TEXT NOT NULL + ); + """ + ) + + """ + Initial orders table. + """ + await db.execute( + f""" + CREATE TABLE market.orders ( + id {db.serial_primary_key}, + wallet TEXT NOT NULL, + username TEXT, + pubkey TEXT, + shippingzone TEXT NOT NULL, + address TEXT NOT NULL, + email TEXT NOT NULL, + total INTEGER NOT NULL, + invoiceid TEXT NOT NULL, + paid BOOLEAN NOT NULL, + shipped BOOLEAN NOT NULL, + time TIMESTAMP NOT NULL DEFAULT """ + + db.timestamp_now + + """ + ); + """ + ) + + """ + Initial order details table. + """ + await db.execute( + f""" + CREATE TABLE market.order_details ( + id TEXT PRIMARY KEY, + order_id INTEGER NOT NULL REFERENCES {db.references_schema}orders (id) ON DELETE CASCADE, + product_id TEXT NOT NULL REFERENCES {db.references_schema}products (id) ON DELETE CASCADE, + quantity INTEGER NOT NULL + ); + """ + ) + + """ + Initial market table. + """ + await db.execute( + """ + CREATE TABLE market.markets ( + id TEXT PRIMARY KEY, + usr TEXT NOT NULL, + name TEXT + ); + """ + ) + + """ + Initial market stalls table. + """ + await db.execute( + f""" + CREATE TABLE market.market_stalls ( + id TEXT PRIMARY KEY, + marketid TEXT NOT NULL REFERENCES {db.references_schema}markets (id) ON DELETE CASCADE, + stallid TEXT NOT NULL REFERENCES {db.references_schema}stalls (id) ON DELETE CASCADE + ); + """ + ) + + """ + Initial chat messages table. + """ + await db.execute( + f""" + CREATE TABLE market.messages ( + id {db.serial_primary_key}, + msg TEXT NOT NULL, + pubkey TEXT NOT NULL, + id_conversation TEXT NOT NULL, + timestamp TIMESTAMP NOT NULL DEFAULT """ + + db.timestamp_now + + """ + ); + """ + ) + + if db.type != "SQLITE": + """ + Create indexes for message fetching + """ + await db.execute( + "CREATE INDEX idx_messages_timestamp ON market.messages (timestamp DESC)" + ) + await db.execute( + "CREATE INDEX idx_messages_conversations ON market.messages (id_conversation)" + ) diff --git a/lnbits/extensions/market/models.py b/lnbits/extensions/market/models.py new file mode 100644 index 00000000..ea7f6f20 --- /dev/null +++ b/lnbits/extensions/market/models.py @@ -0,0 +1,135 @@ +from typing import List, Optional + +from fastapi.param_functions import Query +from pydantic import BaseModel + + +class MarketSettings(BaseModel): + user: str + currency: str + fiat_base_multiplier: int + + +class SetSettings(BaseModel): + currency: str + fiat_base_multiplier: int = Query(100, ge=1) + + +class Stalls(BaseModel): + id: str + wallet: str + name: str + currency: str + publickey: Optional[str] + relays: Optional[str] + shippingzones: str + + +class createStalls(BaseModel): + wallet: str = Query(...) + name: str = Query(...) + currency: str = Query("sat") + publickey: str = Query(None) + relays: str = Query(None) + shippingzones: str = Query(...) + + +class createProduct(BaseModel): + stall: str = Query(...) + product: str = Query(...) + categories: str = Query(None) + description: str = Query(None) + image: str = Query(None) + price: float = Query(0, ge=0) + quantity: int = Query(0, ge=0) + + +class Products(BaseModel): + id: str + stall: str + product: str + categories: Optional[str] + description: Optional[str] + image: Optional[str] + price: float + quantity: int + + +class createZones(BaseModel): + cost: float = Query(0, ge=0) + countries: str = Query(...) + + +class Zones(BaseModel): + id: str + user: str + cost: float + countries: str + + +class OrderDetail(BaseModel): + id: str + order_id: str + product_id: str + quantity: int + + +class createOrderDetails(BaseModel): + product_id: str = Query(...) + quantity: int = Query(..., ge=1) + + +class createOrder(BaseModel): + wallet: str = Query(...) + username: str = Query(None) + pubkey: str = Query(None) + shippingzone: str = Query(...) + address: str = Query(...) + email: str = Query(...) + total: int = Query(...) + products: List[createOrderDetails] + + +class Orders(BaseModel): + id: str + wallet: str + username: Optional[str] + pubkey: Optional[str] + shippingzone: str + address: str + email: str + total: int + invoiceid: str + paid: bool + shipped: bool + time: int + + +class CreateMarket(BaseModel): + usr: str = Query(...) + name: str = Query(None) + stalls: List[str] = Query(...) + + +class Market(BaseModel): + id: str + usr: str + name: Optional[str] + + +class CreateMarketStalls(BaseModel): + stallid: str + + +class ChatMessage(BaseModel): + id: str + msg: str + pubkey: str + id_conversation: str + timestamp: int + + +class CreateChatMessage(BaseModel): + msg: str = Query(..., min_length=1) + pubkey: str = Query(...) + room_name: str = Query(...) diff --git a/lnbits/extensions/market/notifier.py b/lnbits/extensions/market/notifier.py new file mode 100644 index 00000000..e2bf7c91 --- /dev/null +++ b/lnbits/extensions/market/notifier.py @@ -0,0 +1,91 @@ +## adapted from https://github.com/Sentymental/chat-fastapi-websocket +""" +Create a class Notifier that will handle messages +and delivery to the specific person +""" + +import json +from collections import defaultdict + +from fastapi import WebSocket +from loguru import logger + +from lnbits.extensions.market.crud import create_chat_message +from lnbits.extensions.market.models import CreateChatMessage + + +class Notifier: + """ + Manages chatrooms, sessions and members. + + Methods: + - get_notification_generator(self): async generator with notification messages + - get_members(self, room_name: str): get members in room + - push(message: str, room_name: str): push message + - connect(websocket: WebSocket, room_name: str): connect to room + - remove(websocket: WebSocket, room_name: str): remove + - _notify(message: str, room_name: str): notifier + """ + + def __init__(self): + # Create sessions as a dict: + self.sessions: dict = defaultdict(dict) + + # Create notification generator: + self.generator = self.get_notification_generator() + + async def get_notification_generator(self): + """Notification Generator""" + + while True: + message = yield + msg = message["message"] + room_name = message["room_name"] + await self._notify(msg, room_name) + + def get_members(self, room_name: str): + """Get all members in a room""" + + try: + logger.info(f"Looking for members in room: {room_name}") + return self.sessions[room_name] + + except Exception: + logger.exception(f"There is no member in room: {room_name}") + return None + + async def push(self, message: str, room_name: str = None): + """Push a message""" + + message_body = {"message": message, "room_name": room_name} + await self.generator.asend(message_body) + + async def connect(self, websocket: WebSocket, room_name: str): + """Connect to room""" + + await websocket.accept() + if self.sessions[room_name] == {} or len(self.sessions[room_name]) == 0: + self.sessions[room_name] = [] + + self.sessions[room_name].append(websocket) + print(f"Connections ...: {self.sessions[room_name]}") + + def remove(self, websocket: WebSocket, room_name: str): + """Remove websocket from room""" + + self.sessions[room_name].remove(websocket) + print(f"Connection removed...\nOpen connections...: {self.sessions[room_name]}") + + async def _notify(self, message: str, room_name: str): + """Notifier""" + d = json.loads(message) + d["room_name"] = room_name + db_msg = CreateChatMessage.parse_obj(d) + await create_chat_message(data=db_msg) + + remaining_sessions = [] + while len(self.sessions[room_name]) > 0: + websocket = self.sessions[room_name].pop() + await websocket.send_text(message) + remaining_sessions.append(websocket) + self.sessions[room_name] = remaining_sessions diff --git a/lnbits/extensions/market/static/images/bitcoin-shop.png b/lnbits/extensions/market/static/images/bitcoin-shop.png new file mode 100644 index 00000000..debffbb2 Binary files /dev/null and b/lnbits/extensions/market/static/images/bitcoin-shop.png differ diff --git a/lnbits/extensions/market/static/images/placeholder.png b/lnbits/extensions/market/static/images/placeholder.png new file mode 100644 index 00000000..c7d3a947 Binary files /dev/null and b/lnbits/extensions/market/static/images/placeholder.png differ diff --git a/lnbits/extensions/market/tasks.py b/lnbits/extensions/market/tasks.py new file mode 100644 index 00000000..004ebb4d --- /dev/null +++ b/lnbits/extensions/market/tasks.py @@ -0,0 +1,42 @@ +import asyncio + +from loguru import logger + +from lnbits.core.models import Payment +from lnbits.tasks import register_invoice_listener + +from .crud import ( + get_market_order_details, + get_market_order_invoiceid, + set_market_order_paid, + update_market_product_stock, +) + + +async def wait_for_paid_invoices(): + invoice_queue = asyncio.Queue() + register_invoice_listener(invoice_queue) + + while True: + payment = await invoice_queue.get() + await on_invoice_paid(payment) + + +async def on_invoice_paid(payment: Payment) -> None: + if not payment.extra: + return + + if payment.extra.get("tag") != "market": + return + + order = await get_market_order_invoiceid(payment.payment_hash) + if not order: + logger.error("this should never happen", payment) + return + + # set order as paid + await set_market_order_paid(payment.payment_hash) + + # deduct items sold from stock + details = await get_market_order_details(order.id) + await update_market_product_stock(details) diff --git a/lnbits/extensions/market/templates/market/_api_docs.html b/lnbits/extensions/market/templates/market/_api_docs.html new file mode 100644 index 00000000..f0d97dbf --- /dev/null +++ b/lnbits/extensions/market/templates/market/_api_docs.html @@ -0,0 +1,128 @@ + + + +
+ LNbits Market (Nostr support coming soon) +
+ +
    +
  1. Create Shipping Zones you're willing to ship to
  2. +
  3. Create a Stall to list yiur products on
  4. +
  5. Create products to put on the Stall
  6. +
  7. Take orders
  8. +
  9. Includes chat support!
  10. +
+ The first LNbits market idea 'Diagon Alley' helped create Nostr, and soon + this market extension will have the option to work on Nostr 'Diagon Alley' + mode, by the merchant, market, and buyer all having keys, and data being + routed through Nostr relays. +
+ + Created by, + Tal Vasconcelos, + Ben Arc + +
+
+
+ + + + + + GET + /market/api/v1/stall/products/<relay_id> +
Body (application/json)
+
+ Returns 201 CREATED (application/json) +
+ Product JSON list +
Curl example
+ curl -X GET {{ request.url_root + }}api/v1/stall/products/<relay_id> +
+
+
+ + + + POST + /market/api/v1/stall/order/<relay_id> +
Body (application/json)
+ {"id": <string>, "address": <string>, "shippingzone": + <integer>, "email": <string>, "quantity": + <integer>} +
+ Returns 201 CREATED (application/json) +
+ {"checking_id": <string>,"payment_request": + <string>} +
Curl example
+ curl -X POST {{ request.url_root + }}api/v1/stall/order/<relay_id> -d '{"id": <product_id&>, + "email": <customer_email>, "address": <customer_address>, + "quantity": 2, "shippingzone": 1}' -H "Content-type: application/json" + +
+
+
+ + + + GET + /market/api/v1/stall/checkshipped/<checking_id> +
Headers
+
+ Returns 200 OK (application/json) +
+ {"shipped": <boolean>} +
Curl example
+ curl -X GET {{ request.url_root + }}api/v1/stall/checkshipped/<checking_id> -H "Content-type: + application/json" +
+
+
+
diff --git a/lnbits/extensions/market/templates/market/_chat_box.html b/lnbits/extensions/market/templates/market/_chat_box.html new file mode 100644 index 00000000..05b0c58f --- /dev/null +++ b/lnbits/extensions/market/templates/market/_chat_box.html @@ -0,0 +1,58 @@ + + +
Messages
+
+ + + + + + + +
+
+ +
+ +
+
+ + + + + + + +
+
+
diff --git a/lnbits/extensions/market/templates/market/_dialogs.html b/lnbits/extensions/market/templates/market/_dialogs.html new file mode 100644 index 00000000..d2a8dd0a --- /dev/null +++ b/lnbits/extensions/market/templates/market/_dialogs.html @@ -0,0 +1,393 @@ + + + + + + + + + + + + + + + + + + + + + + + +
+ Update Product + + Create Product + + Cancel +
+
+
+
+ + + + + + +
+ Update Shipping Zone + Create Shipping Zone + + Cancel +
+
+
+
+ + + + + + + +
+ Update Marketplace + Launch Marketplace + + Cancel +
+
+
+
+ + + + + + + + + + +
+
+ Generate keys +
+
+ Restore keys +
+
+ + + + + +
+ Update Stall + Create Stall + Cancel +
+
+
+
+ + + +
How to use Market
+ + + Create Shipping Zones you're willing to ship to. You can define + different values for different zones. + + + + + + Create a Stall and provide private and public keys to use for + communication. If you don't have one, LNbits will create a key pair for + you. It will be saved and can be used on other stalls. + + + + + + + Create your products, add a small description and an image. Choose to + what stall, if you have more than one, it belongs to + + + +
+ +
+
+
+
+
diff --git a/lnbits/extensions/market/templates/market/_tables.html b/lnbits/extensions/market/templates/market/_tables.html new file mode 100644 index 00000000..c6fd665b --- /dev/null +++ b/lnbits/extensions/market/templates/market/_tables.html @@ -0,0 +1,440 @@ + + + +
+
+
Orders
+
+
+ Export to CSV +
+
+ + {% raw %} + + + {% endraw %} + +
+
+ + + + +
+
+
+ Products + + + Add a product + +
+
+
+ Export to CSV +
+
+ + {% raw %} + + + {% endraw %} + +
+
+ + + + +
+
+
Market Stalls
+
+
+ Export to CSV +
+
+ + {% raw %} + + + {% endraw %} + +
+
+ + + + +
+
+
Marketplaces
+
+
+ Export to CSV +
+
+ + {% raw %} + + + {% endraw %} + +
+
+ + + + +
+
+
Shipping Zones
+
+
+ Export to CSV +
+
+ + {% raw %} + + + {% endraw %} + +
+
diff --git a/lnbits/extensions/market/templates/market/index.html b/lnbits/extensions/market/templates/market/index.html new file mode 100644 index 00000000..ffcb612b --- /dev/null +++ b/lnbits/extensions/market/templates/market/index.html @@ -0,0 +1,1419 @@ +{% extends "base.html" %} {% from "macros.jinja" import window_vars with context +%} {% block page %} +
+ {% include "market/_dialogs.html" %} +
+ + + + Shipping Zone Create a shipping zone + + Stall + + Create a market stall to list products on + + + Stall + + Create a market stall to list products on + + + Product List a product + + Product List a product + + Create Market + + Makes a simple frontend market for your stalls (not + NOSTR) + + + +
Market
+
Make a market of multiple stalls.
+
+ + + + Coming soon... + Export all Data + + Export all data (markets, products, orders, etc...) + +
+ + {% include "market/_tables.html" %} + + + +
+
+
Keys
+
+
+ Export to CSV +
+
+
+ +
+
+
+ {% raw %} + + + {{ keys[type] }} + +

+ {{ type == 'pubkey' ? 'Public Key' : 'Private Key' }}
Click to copy +

+ {% endraw %} +
+
+
+
+
+
+ +
+ + +
+ LNbits Market Extension (Nostr support coming soon) +
+
+ + + {% include "market/_api_docs.html" %} + +
+ + {% include "market/_chat_box.html" %} +
+
+ +{% endblock %} {% block scripts %} {{ window_vars(user) }} + + + + + + +{% endblock %} diff --git a/lnbits/extensions/market/templates/market/market.html b/lnbits/extensions/market/templates/market/market.html new file mode 100644 index 00000000..e59bb245 --- /dev/null +++ b/lnbits/extensions/market/templates/market/market.html @@ -0,0 +1,175 @@ +{% extends "public.html" %} {% block page %} +
+
+ +
+ Market: {{ market.name }} +
+
+ + + +
+
+
+
+
+
+ + {% raw %} + + + +
+
+ {{ item.product }} +
+
+ + +
+ + +
+
+ {{ item.stallName }} +
+ + {{ item.price }} satsBTC {{ (item.price / 1e8).toFixed(8) }} + + + {{ getAmountFormated(item.price, item.currency) }} + ({{ getValueInSats(item.price, item.currency) }} sats) + + {{item.quantity}} left +
+
+ {{cat}} +
+
+

{{ item.description }}

+
+
+ + + + + Stall: {{ item.stallName }} + + Visit Stall + + + {% endraw %} +
+
+
+{% endblock %} {% block scripts %} + +{% endblock %} diff --git a/lnbits/extensions/market/templates/market/order.html b/lnbits/extensions/market/templates/market/order.html new file mode 100644 index 00000000..5be606f9 --- /dev/null +++ b/lnbits/extensions/market/templates/market/order.html @@ -0,0 +1,564 @@ +{% extends "public.html" %} {% block page %} +
+
+ +
+
+ +
+ +
+
+ + + + + +
+
+
+
+ + + {% raw %} +
{{ stall.name }}
+

+ Public Key: {{ sliceKey(stall.publickey) }} + Click to copy +

+ {% endraw %} +
+ + + + + + + + + {% raw %} + + + {{p.quantity}} x + + + + + + + + {{ p.name }} + + + + {{ getAmountFormated(p.price) }} + {{p.price}} sats + + + {% endraw %} + + + + + +

+ Bellow are the keys needed to contact the merchant. They are + stored in the browser! +

+
+
+
+ {% raw %} + + + {{ user.keys[type] }} + +

+ {{ type == 'publickey' ? 'Public Key' : 'Private Key' }} +

+ {% endraw %} +
+
+
+ +
+ Backup keys + Download your keys + + Restore keys + Restore keys + + Delete data + Delete all data from browser + +
+
+
+ +

Export, or send, this page to another device

+
+ + + Click to copy + +
+
+ Copy URL + Export, or send, this page to another device + +
+
+
+
+
+ + + + + + + +
+ + +
+
+
+
+ + + +
Bookmark this page
+

+ Don't forget to bookmark this page to be able to check on your order! +

+

+ You can backup your keys, and export the page to another device also. +

+
+ Close +
+
+
+
+{% endblock %} {% block scripts %} + + + + +{% endblock %} diff --git a/lnbits/extensions/market/templates/market/product.html b/lnbits/extensions/market/templates/market/product.html new file mode 100644 index 00000000..66f56691 --- /dev/null +++ b/lnbits/extensions/market/templates/market/product.html @@ -0,0 +1,14 @@ +{% extends "public.html" %} {% block page %} +

Product page

+{% endblock %} {% block scripts %} + +{% endblock %} diff --git a/lnbits/extensions/market/templates/market/stall.html b/lnbits/extensions/market/templates/market/stall.html new file mode 100644 index 00000000..f9189b30 --- /dev/null +++ b/lnbits/extensions/market/templates/market/stall.html @@ -0,0 +1,531 @@ +{% extends "public.html" %} {% block page %} +
+
+ +
+ Stall: {{ stall.name }} +
+
+ + + +
+ + {% raw %} + + {{ cart.size }} + + {% endraw %} + + + {% raw %} + + + {{p.quantity}} x + + + + + + + + + {{ p.name }} + + + + + {{unit != 'sat' ? getAmountFormated(p.price) : p.price + + 'sats'}} + + + + + {% endraw %} + + +
+ + +
+
+
+
+
+
+
+
+ + {% raw %} + + + + Add to cart + +
+
+ {{ item.product }} +
+
+ + +
+ + +
+ + {{ item.price }} satsBTC {{ (item.price / 1e8).toFixed(8) }} + + + {{ getAmountFormated(item.price) }} + ({{ getValueInSats(item.price) }} sats) + + {{item.quantity}} left +
+
+ {{cat}} +
+
+

{{ item.description }}

+
+
+ + + {% endraw %} +
+
+ + + + + + + + + + + +

Select the shipping zone:

+
+ +
+
+ {% raw %} Total: {{ unit != 'sat' ? getAmountFormated(finalCost) : + finalCost + 'sats' }} + ({{ getValueInSats(finalCost) }} sats) + {% endraw %} +
+
+ Checkout + Cancel +
+
+
+
+ + + + + + +
+ Copy invoice + Close +
+
+
+
+{% endblock %} {% block scripts %} + +{% endblock %} diff --git a/lnbits/extensions/market/views.py b/lnbits/extensions/market/views.py new file mode 100644 index 00000000..23bc5706 --- /dev/null +++ b/lnbits/extensions/market/views.py @@ -0,0 +1,177 @@ +import json +from http import HTTPStatus +from typing import List + +from fastapi import ( + BackgroundTasks, + Depends, + Query, + Request, + WebSocket, + WebSocketDisconnect, +) +from fastapi.templating import Jinja2Templates +from loguru import logger +from starlette.exceptions import HTTPException +from starlette.responses import HTMLResponse + +from lnbits.core.models import User +from lnbits.decorators import check_user_exists # type: ignore +from lnbits.extensions.market import market_ext, market_renderer +from lnbits.extensions.market.models import CreateChatMessage, SetSettings +from lnbits.extensions.market.notifier import Notifier + +from .crud import ( + create_chat_message, + create_market_settings, + get_market_market, + get_market_market_stalls, + get_market_order_details, + get_market_order_invoiceid, + get_market_products, + get_market_settings, + get_market_stall, + get_market_zone, + get_market_zones, + update_market_product_stock, +) + +templates = Jinja2Templates(directory="templates") + + +@market_ext.get("/", response_class=HTMLResponse) +async def index(request: Request, user: User = Depends(check_user_exists)): + settings = await get_market_settings(user=user.id) + + if not settings: + await create_market_settings( + user=user.id, data=SetSettings(currency="sat", fiat_base_multiplier=1) + ) + settings = await get_market_settings(user.id) + assert settings + return market_renderer().TemplateResponse( + "market/index.html", + {"request": request, "user": user.dict(), "currency": settings.currency}, + ) + + +@market_ext.get("/stalls/{stall_id}", response_class=HTMLResponse) +async def stall(request: Request, stall_id): + stall = await get_market_stall(stall_id) + products = await get_market_products(stall_id) + + if not stall: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="Stall does not exist." + ) + + zones = [] + for id in stall.shippingzones.split(","): + zone = await get_market_zone(id) + assert zone + z = zone.dict() + zones.append({"label": z["countries"], "cost": z["cost"], "value": z["id"]}) + + _stall = stall.dict() + + _stall["zones"] = zones + + return market_renderer().TemplateResponse( + "market/stall.html", + { + "request": request, + "stall": _stall, + "products": [product.dict() for product in products], + }, + ) + + +@market_ext.get("/market/{market_id}", response_class=HTMLResponse) +async def market(request: Request, market_id): + market = await get_market_market(market_id) + + if not market: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="Marketplace doesn't exist." + ) + + stalls = await get_market_market_stalls(market_id) + stalls_ids = [stall.id for stall in stalls] + products = [product.dict() for product in await get_market_products(stalls_ids)] + + return market_renderer().TemplateResponse( + "market/market.html", + { + "request": request, + "market": market, + "stalls": [stall.dict() for stall in stalls], + "products": products, + }, + ) + + +@market_ext.get("/order", response_class=HTMLResponse) +async def order_chat( + request: Request, + merch: str = Query(...), + invoice_id: str = Query(...), + keys: str = Query(None), +): + stall = await get_market_stall(merch) + assert stall + order = await get_market_order_invoiceid(invoice_id) + assert order + _order = await get_market_order_details(order.id) + products = await get_market_products(stall.id) + assert products + + return market_renderer().TemplateResponse( + "market/order.html", + { + "request": request, + "stall": { + "id": stall.id, + "name": stall.name, + "publickey": stall.publickey, + "wallet": stall.wallet, + "currency": stall.currency, + }, + "publickey": keys.split(",")[0] if keys else None, + "privatekey": keys.split(",")[1] if keys else None, + "order_id": order.invoiceid, + "order": [details.dict() for details in _order], + "products": [product.dict() for product in products], + }, + ) + + +##################WEBSOCKET ROUTES######################## + +# Initialize Notifier: +notifier = Notifier() + + +@market_ext.websocket("/ws/{room_name}") +async def websocket_endpoint( + websocket: WebSocket, room_name: str, background_tasks: BackgroundTasks +): + await notifier.connect(websocket, room_name) + try: + while True: + data = await websocket.receive_text() + d = json.loads(data) + d["room_name"] = room_name + + room_members = ( + notifier.get_members(room_name) + if notifier.get_members(room_name) is not None + else [] + ) + + if websocket not in room_members: + print("Sender not in room member: Reconnecting...") + await notifier.connect(websocket, room_name) + await notifier._notify(data, room_name) + + except WebSocketDisconnect: + notifier.remove(websocket, room_name) diff --git a/lnbits/extensions/market/views_api.py b/lnbits/extensions/market/views_api.py new file mode 100644 index 00000000..045bc0fc --- /dev/null +++ b/lnbits/extensions/market/views_api.py @@ -0,0 +1,518 @@ +from base64 import urlsafe_b64encode +from http import HTTPStatus +from typing import List, Union +from uuid import uuid4 + +from fastapi import Body, Depends, Query, Request +from loguru import logger +from starlette.exceptions import HTTPException + +from lnbits.core.crud import get_user +from lnbits.core.services import create_invoice +from lnbits.core.views.api import api_payment +from lnbits.decorators import ( + WalletTypeInfo, + get_key_type, + require_admin_key, + require_invoice_key, +) +from lnbits.helpers import urlsafe_short_hash +from lnbits.utils.exchange_rates import currencies, get_fiat_rate_satoshis + +from . import db, market_ext +from .crud import ( + create_market_market, + create_market_market_stalls, + create_market_order, + create_market_order_details, + create_market_product, + create_market_settings, + create_market_stall, + create_market_zone, + delete_market_order, + delete_market_product, + delete_market_stall, + delete_market_zone, + get_market_chat_by_merchant, + get_market_chat_messages, + get_market_latest_chat_messages, + get_market_market, + get_market_market_stalls, + get_market_markets, + get_market_order, + get_market_order_details, + get_market_order_invoiceid, + get_market_orders, + get_market_product, + get_market_products, + get_market_settings, + get_market_stall, + get_market_stalls, + get_market_stalls_by_ids, + get_market_zone, + get_market_zones, + set_market_order_pubkey, + set_market_settings, + update_market_market, + update_market_product, + update_market_stall, + update_market_zone, +) +from .models import ( + CreateMarket, + CreateMarketStalls, + Orders, + Products, + SetSettings, + Stalls, + Zones, + createOrder, + createProduct, + createStalls, + createZones, +) + +# from lnbits.db import open_ext_db + + +### Products +@market_ext.get("/api/v1/products") +async def api_market_products( + wallet: WalletTypeInfo = Depends(require_invoice_key), + all_stalls: bool = Query(False), +): + wallet_ids = [wallet.wallet.id] + + if all_stalls: + user = await get_user(wallet.wallet.user) + wallet_ids = user.wallet_ids if user else [] + + stalls = [stall.id for stall in await get_market_stalls(wallet_ids)] + + if not stalls: + return + + return [product.dict() for product in await get_market_products(stalls)] + + +@market_ext.post("/api/v1/products") +@market_ext.put("/api/v1/products/{product_id}") +async def api_market_product_create( + data: createProduct, + product_id=None, + wallet: WalletTypeInfo = Depends(require_invoice_key), +): + # For fiat currencies, + # we multiply by data.fiat_base_multiplier (usually 100) to save the value in cents. + settings = await get_market_settings(user=wallet.wallet.user) + assert settings + + stall = await get_market_stall(stall_id=data.stall) + assert stall + + if stall.currency != "sat": + data.price *= settings.fiat_base_multiplier + + if product_id: + product = await get_market_product(product_id) + if not product: + return {"message": "Product does not exist."} + + # stall = await get_market_stall(stall_id=product.stall) + if stall.wallet != wallet.wallet.id: + return {"message": "Not your product."} + + product = await update_market_product(product_id, **data.dict()) + else: + product = await create_market_product(data=data) + assert product + return product.dict() + + +@market_ext.delete("/api/v1/products/{product_id}") +async def api_market_products_delete( + product_id, wallet: WalletTypeInfo = Depends(require_admin_key) +): + product = await get_market_product(product_id) + + if not product: + return {"message": "Product does not exist."} + + stall = await get_market_stall(product.stall) + assert stall + + if stall.wallet != wallet.wallet.id: + return {"message": "Not your Market."} + + await delete_market_product(product_id) + raise HTTPException(status_code=HTTPStatus.NO_CONTENT) + + +# # # Shippingzones + + +@market_ext.get("/api/v1/zones") +async def api_market_zones(wallet: WalletTypeInfo = Depends(get_key_type)): + + return await get_market_zones(wallet.wallet.user) + + +@market_ext.post("/api/v1/zones") +async def api_market_zone_create( + data: createZones, wallet: WalletTypeInfo = Depends(get_key_type) +): + zone = await create_market_zone(user=wallet.wallet.user, data=data) + return zone.dict() + + +@market_ext.post("/api/v1/zones/{zone_id}") +async def api_market_zone_update( + data: createZones, + zone_id: str, + wallet: WalletTypeInfo = Depends(require_admin_key), +): + zone = await get_market_zone(zone_id) + if not zone: + return {"message": "Zone does not exist."} + if zone.user != wallet.wallet.user: + return {"message": "Not your record."} + zone = await update_market_zone(zone_id, **data.dict()) + return zone + + +@market_ext.delete("/api/v1/zones/{zone_id}") +async def api_market_zone_delete( + zone_id, wallet: WalletTypeInfo = Depends(require_admin_key) +): + zone = await get_market_zone(zone_id) + + if not zone: + return {"message": "zone does not exist."} + + if zone.user != wallet.wallet.user: + return {"message": "Not your zone."} + + await delete_market_zone(zone_id) + raise HTTPException(status_code=HTTPStatus.NO_CONTENT) + + +# # # Stalls + + +@market_ext.get("/api/v1/stalls") +async def api_market_stalls( + wallet: WalletTypeInfo = Depends(get_key_type), all_wallets: bool = Query(False) +): + wallet_ids = [wallet.wallet.id] + + if all_wallets: + user = await get_user(wallet.wallet.user) + wallet_ids = user.wallet_ids if user else [] + + return [stall.dict() for stall in await get_market_stalls(wallet_ids)] + + +@market_ext.post("/api/v1/stalls") +@market_ext.put("/api/v1/stalls/{stall_id}") +async def api_market_stall_create( + data: createStalls, + stall_id: str = None, + wallet: WalletTypeInfo = Depends(require_invoice_key), +): + + if stall_id: + stall = await get_market_stall(stall_id) + if not stall: + return {"message": "Withdraw stall does not exist."} + + if stall.wallet != wallet.wallet.id: + return {"message": "Not your withdraw stall."} + + stall = await update_market_stall(stall_id, **data.dict()) + else: + stall = await create_market_stall(data=data) + assert stall + return stall.dict() + + +@market_ext.delete("/api/v1/stalls/{stall_id}") +async def api_market_stall_delete( + stall_id: str, wallet: WalletTypeInfo = Depends(require_admin_key) +): + stall = await get_market_stall(stall_id) + + if not stall: + return {"message": "Stall does not exist."} + + if stall.wallet != wallet.wallet.id: + return {"message": "Not your Stall."} + + await delete_market_stall(stall_id) + raise HTTPException(status_code=HTTPStatus.NO_CONTENT) + + +###Orders + + +@market_ext.get("/api/v1/orders") +async def api_market_orders( + wallet: WalletTypeInfo = Depends(get_key_type), all_wallets: bool = Query(False) +): + wallet_ids = [wallet.wallet.id] + if all_wallets: + user = await get_user(wallet.wallet.user) + wallet_ids = user.wallet_ids if user else [] + + orders = await get_market_orders(wallet_ids) + if not orders: + return + orders_with_details = [] + for order in orders: + _order = order.dict() + _order["details"] = await get_market_order_details(_order["id"]) + orders_with_details.append(_order) + try: + return orders_with_details # [order for order in orders] + # return [order.dict() for order in await get_market_orders(wallet_ids)] + except: + return {"message": "We could not retrieve the orders."} + + +@market_ext.get("/api/v1/orders/{order_id}") +async def api_market_order_by_id(order_id: str): + order = await get_market_order(order_id) + assert order + _order = order.dict() + _order["details"] = await get_market_order_details(order_id) + + return _order + + +@market_ext.post("/api/v1/orders") +async def api_market_order_create(data: createOrder): + ref = urlsafe_short_hash() + + payment_hash, payment_request = await create_invoice( + wallet_id=data.wallet, + amount=data.total, + memo=f"New order on Market", + extra={ + "tag": "market", + "reference": ref, + }, + ) + order_id = await create_market_order(invoiceid=payment_hash, data=data) + logger.debug(f"ORDER ID {order_id}") + logger.debug(f"PRODUCTS {data.products}") + await create_market_order_details(order_id=order_id, data=data.products) + return { + "payment_hash": payment_hash, + "payment_request": payment_request, + "order_reference": ref, + } + + +@market_ext.get("/api/v1/orders/payments/{payment_hash}") +async def api_market_check_payment(payment_hash: str): + order = await get_market_order_invoiceid(payment_hash) + if not order: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="Order does not exist." + ) + try: + status = await api_payment(payment_hash) + + except Exception as exc: + logger.error(exc) + return {"paid": False} + return status + + +@market_ext.delete("/api/v1/orders/{order_id}") +async def api_market_order_delete( + order_id: str, wallet: WalletTypeInfo = Depends(require_admin_key) +): + order = await get_market_order(order_id) + + if not order: + return {"message": "Order does not exist."} + + if order.wallet != wallet.wallet.id: + return {"message": "Not your Order."} + + await delete_market_order(order_id) + + raise HTTPException(status_code=HTTPStatus.NO_CONTENT) + + +# @market_ext.get("/api/v1/orders/paid/{order_id}") +# async def api_market_order_paid( +# order_id, wallet: WalletTypeInfo = Depends(require_admin_key) +# ): +# await db.execute( +# "UPDATE market.orders SET paid = ? WHERE id = ?", +# ( +# True, +# order_id, +# ), +# ) +# return "", HTTPStatus.OK + + +@market_ext.get("/api/v1/order/pubkey/{payment_hash}/{pubkey}") +async def api_market_order_pubkey(payment_hash: str, pubkey: str): + await set_market_order_pubkey(payment_hash, pubkey) + return "", HTTPStatus.OK + + +@market_ext.get("/api/v1/orders/shipped/{order_id}") +async def api_market_order_shipped( + order_id, shipped: bool = Query(...), wallet: WalletTypeInfo = Depends(get_key_type) +): + await db.execute( + "UPDATE market.orders SET shipped = ? WHERE id = ?", + ( + shipped, + order_id, + ), + ) + order = await db.fetchone("SELECT * FROM market.orders WHERE id = ?", (order_id,)) + + return order + + +###List products based on stall id + + +# @market_ext.get("/api/v1/stall/products/{stall_id}") +# async def api_market_stall_products( +# stall_id, wallet: WalletTypeInfo = Depends(get_key_type) +# ): + +# rows = await db.fetchone("SELECT * FROM market.stalls WHERE id = ?", (stall_id,)) +# if not rows: +# return {"message": "Stall does not exist."} + +# products = db.fetchone("SELECT * FROM market.products WHERE wallet = ?", (rows[1],)) +# if not products: +# return {"message": "No products"} + +# return [products.dict() for products in await get_market_products(rows[1])] + + +###Check a product has been shipped + + +# @market_ext.get("/api/v1/stall/checkshipped/{checking_id}") +# async def api_market_stall_checkshipped( +# checking_id, wallet: WalletTypeInfo = Depends(get_key_type) +# ): +# rows = await db.fetchone( +# "SELECT * FROM market.orders WHERE invoiceid = ?", (checking_id,) +# ) +# return {"shipped": rows["shipped"]} + + +## +# MARKETS +## + + +@market_ext.get("/api/v1/markets") +async def api_market_markets(wallet: WalletTypeInfo = Depends(get_key_type)): + # await get_market_market_stalls(market_id="FzpWnMyHQMcRppiGVua4eY") + try: + return [ + market.dict() for market in await get_market_markets(wallet.wallet.user) + ] + except: + return {"message": "We could not retrieve the markets."} + + +@market_ext.get("/api/v1/markets/{market_id}/stalls") +async def api_market_market_stalls(market_id: str): + stall_ids = await get_market_market_stalls(market_id) + return stall_ids + + +@market_ext.post("/api/v1/markets") +@market_ext.put("/api/v1/markets/{market_id}") +async def api_market_market_create( + data: CreateMarket, + market_id: str = None, + wallet: WalletTypeInfo = Depends(require_invoice_key), +): + if market_id: + market = await get_market_market(market_id) + if not market: + return {"message": "Market does not exist."} + + if market.usr != wallet.wallet.user: + return {"message": "Not your market."} + + market = await update_market_market(market_id, data.name) + else: + market = await create_market_market(data=data) + + assert market + await create_market_market_stalls(market_id=market.id, data=data.stalls) + + return market.dict() + + +## MESSAGES/CHAT + + +@market_ext.get("/api/v1/chat/messages/merchant") +async def api_get_merchant_messages( + orders: str = Query(...), wallet: WalletTypeInfo = Depends(require_admin_key) +): + return [msg.dict() for msg in await get_market_chat_by_merchant(orders.split(","))] + + +@market_ext.get("/api/v1/chat/messages/{room_name}") +async def api_get_latest_chat_msg(room_name: str, all_messages: bool = Query(False)): + if all_messages: + messages = await get_market_chat_messages(room_name) + else: + messages = await get_market_latest_chat_messages(room_name) + + return messages + + +@market_ext.get("/api/v1/currencies") +async def api_list_currencies_available(): + return list(currencies.keys()) + + +@market_ext.get("/api/v1/settings") +async def api_get_settings(wallet: WalletTypeInfo = Depends(require_admin_key)): + user = wallet.wallet.user + + settings = await get_market_settings(user) + + return settings + + +@market_ext.post("/api/v1/settings") +@market_ext.put("/api/v1/settings/{usr}") +async def api_set_settings( + data: SetSettings, + usr: str = None, + wallet: WalletTypeInfo = Depends(require_admin_key), +): + if usr: + if usr != wallet.wallet.user: + return {"message": "Not your Market."} + + settings = await get_market_settings(user=usr) + assert settings + + if settings.user != wallet.wallet.user: + return {"message": "Not your Market."} + + return await set_market_settings(usr, data) + + user = wallet.wallet.user + + return await create_market_settings(user, data)