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 @@ +
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 @@
+GET
+ /market/api/v1/stall/products/<relay_id>
+ Product JSON list
+ curl -X GET {{ request.url_root
+ }}api/v1/stall/products/<relay_id>
+ POST
+ /market/api/v1/stall/order/<relay_id>
+ {"id": <string>, "address": <string>, "shippingzone":
+ <integer>, "email": <string>, "quantity":
+ <integer>}
+ {"checking_id": <string>,"payment_request":
+ <string>}
+ 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>
+ {"shipped": <boolean>}
+ curl -X GET {{ request.url_root
+ }}api/v1/stall/checkshipped/<checking_id> -H "Content-type:
+ application/json"
+
+ {{ type == 'pubkey' ? 'Public Key' : 'Private Key' }}
Click to copy
+
+ Public Key: {{ sliceKey(stall.publickey) }}
+
+ Bellow are the keys needed to contact the merchant. They are + stored in the browser! +
++ {{ type == 'publickey' ? 'Public Key' : 'Private Key' }} +
+ {% endraw %} +Export, or send, this page to another device
++ 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. +
+Select the shipping zone:
+