From 2d60683beb4ca1b1e347d8997c5feeac37e63e3e Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Mon, 27 Feb 2023 17:45:08 +0000 Subject: [PATCH 001/891] Add files via upload --- README.md | 281 ++++++++++++++++++++++++++++++++++++++++++++++ migrations.py | 302 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 583 insertions(+) create mode 100644 README.md create mode 100644 migrations.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..88e796f --- /dev/null +++ b/README.md @@ -0,0 +1,281 @@ +## Nostr Diagon Alley protocol (for resilient marketplaces) + +#### Original protocol https://github.com/lnbits/Diagon-Alley + +> The concepts around resilience in Diagon Alley helped influence the creation of the NOSTR protocol, now we get to build Diagon Alley on NOSTR! + +In Diagon Alley, `merchant` and `customer` communicate via NOSTR relays, so loss of money, product information, and reputation become far less likely if attacked. + +A `merchant` and `customer` both have a NOSTR key-pair that are used to sign notes and subscribe to events. + +#### For further information about NOSTR, see https://github.com/nostr-protocol/nostr + + +## Terms + +* `merchant` - seller of products with NOSTR key-pair +* `customer` - buyer of products with NOSTR key-pair +* `product` - item for sale by the `merchant` +* `stall` - list of products controlled by `merchant` (a `merchant` can have multiple stalls) +* `marketplace` - clientside software for searching `stalls` and purchasing `products` + +## Diagon Alley Clients + +### Merchant admin + +Where the `merchant` creates, updates and deletes `stalls` and `products`, as well as where they manage sales, payments and communication with `customers`. + +The `merchant` admin software can be purely clientside, but for `convenience` and uptime, implementations will likely have a server listening for NOSTR events. + +### Marketplace + +`Marketplace` software should be entirely clientside, either as a stand-alone app, or as a purely frontend webpage. A `customer` subscribes to different merchant NOSTR public keys, and those `merchants` `stalls` and `products` become listed and searchable. The marketplace client is like any other ecommerce site, with basket and checkout. `Marketplaces` may also wish to include a `customer` support area for direct message communication with `merchants`. + +## `Merchant` publishing/updating products (event) + +NIP-01 https://github.com/nostr-protocol/nips/blob/master/01.md uses the basic NOSTR event type. + +The `merchant` event that publishes and updates product lists + +The below json goes in `content` of NIP-01. + +Data from newer events should replace data from older events. + +`action` types (used to indicate changes): +* `update` element has changed +* `delete` element should be deleted +* `suspend` element is suspended +* `unsuspend` element is unsuspended + + +``` +{ + "name": , + "description": , + "currency": , + "action": , + "shipping": [ + { + "id": , + "zones": , + "price": , + }, + { + "id": , + "zones": , + "price": , + }, + { + "id": , + "zones": , + "price": , + } + ], + "stalls": [ + { + "id": , + "name": , + "description": , + "categories": , + "shipping": , + "action": , + "products": [ + { + "id": , + "name": , + "description": , + "categories": , + "amount": , + "price": , + "images": [ + { + "id": , + "name": , + "link": + } + ], + "action": , + }, + { + "id": , + "name": , + "description": , + "categories": , + "amount": , + "price": , + "images": [ + { + "id": , + "name": , + "link": + }, + { + "id": , + "name": , + "link": + } + ], + "action": , + }, + ] + }, + { + "id": , + "name": , + "description": , + "categories": , + "shipping": , + "action": , + "products": [ + { + "id": , + "name": , + "categories": , + "amount": , + "price": , + "images": [ + { + "id": , + "name": , + "link": + } + ], + "action": , + } + ] + } + ] +} + +``` + +As all elements are optional, an `update` `action` to a `product` `image`, may look as simple as: + +``` +{ + "stalls": [ + { + "id": , + "products": [ + { + "id": , + "images": [ + { + "id": , + "name": , + "link": + } + ], + "action": , + }, + ] + } + ] +} + +``` + + +## Checkout events + +NIP-04 https://github.com/nostr-protocol/nips/blob/master/04.md, all checkout events are encrypted + +The below json goes in `content` of NIP-04. + +### Step 1: `customer` order (event) + + +``` +{ + "id": , + "name": , + "description": , + "address": , + "message": , + "contact": [ + "nostr": , + "phone": , + "email": + ], + "items": [ + { + "id": , + "quantity": , + "message": + }, + { + "id": , + "quantity": , + "message": + }, + { + "id": , + "quantity": , + "message": + } + +} + +``` + +Merchant should verify the sum of product ids + timestamp. + +### Step 2: `merchant` request payment (event) + +Sent back from the merchant for payment. Any payment option is valid that the merchant can check. + +The below json goes in `content` of NIP-04. + +`payment_options`/`type` include: +* `url` URL to a payment page, stripe, paypal, btcpayserver, etc +* `btc` onchain bitcoin address +* `ln` bitcoin lightning invoice +* `lnurl` bitcoin lnurl-pay + +``` +{ + "id": , + "message": , + "payment_options": [ + { + "type": , + "link": + }, + { + "type": , + "link": + }, + { + "type": , + "link": + } +} + +``` + +### Step 3: `merchant` verify payment/shipped (event) + +Once payment has been received and processed. + +The below json goes in `content` of NIP-04. + +``` +{ + "id": , + "message": , + "paid": , + "shipped": , +} + +``` + +## Customer support events + +Customer support is handle over whatever communication method was specified. If communicationg via nostr, NIP-04 is used https://github.com/nostr-protocol/nips/blob/master/04.md. + +## Additional + +Standard data models can be found here here + + + diff --git a/migrations.py b/migrations.py new file mode 100644 index 0000000..f7ce7d0 --- /dev/null +++ b/migrations.py @@ -0,0 +1,302 @@ +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)" + ) + + +async def m002_add_custom_relays(db): + """ + Add custom relays to stores + """ + await db.execute("ALTER TABLE market.stalls ADD COLUMN crelays TEXT;") + + +async def m003_fiat_base_multiplier(db): + """ + Store the multiplier for fiat prices. We store the price in cents and + remember to multiply by 100 when we use it to convert to Dollars. + """ + await db.execute( + "ALTER TABLE market.stalls ADD COLUMN fiat_base_multiplier INTEGER DEFAULT 1;" + ) + + await db.execute( + "UPDATE market.stalls SET fiat_base_multiplier = 100 WHERE NOT currency = 'sat';" + ) + + +async def m004_add_privkey_to_stalls(db): + await db.execute("ALTER TABLE market.stalls ADD COLUMN privatekey TEXT") + + +async def m005_add_currency_to_zones(db): + await db.execute("ALTER TABLE market.zones ADD COLUMN stall TEXT") + await db.execute("ALTER TABLE market.zones ADD COLUMN currency TEXT DEFAULT 'sat'") + + +async def m006_delete_market_settings(db): + await db.execute("DROP TABLE market.settings") + + +async def m007_order_id_to_UUID(db): + """ + Migrate ID column type to string for UUIDs and migrate existing data + """ + + await db.execute("ALTER TABLE market.orders RENAME TO orders_old") + await db.execute( + f""" + CREATE TABLE market.orders ( + id TEXT 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 + + """ + ); + """ + ) + + for row in [ + list(row) for row in await db.fetchall("SELECT * FROM market.orders_old") + ]: + await db.execute( + """ + INSERT INTO market.orders ( + id, + wallet, + username, + pubkey, + shippingzone, + address, + email, + total, + invoiceid, + paid, + shipped, + time + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, + ( + str(row[0]), + row[1], + row[2], + row[3], + row[4], + row[5], + row[6], + row[7], + row[8], + row[9], + row[10], + int(row[11]), + ), + ) + + await db.execute("DROP TABLE market.orders_old") + + +async def m008_message_id_to_TEXT(db): + """ + Migrate ID column type to string for UUIDs and migrate existing data + """ + + await db.execute("ALTER TABLE market.messages RENAME TO messages_old") + await db.execute( + f""" + CREATE TABLE market.messages ( + id TEXT PRIMARY KEY, + msg TEXT NOT NULL, + pubkey TEXT NOT NULL, + id_conversation TEXT NOT NULL, + timestamp TIMESTAMP NOT NULL DEFAULT """ + + db.timestamp_now + + """ + ); + """ + ) + + for row in [ + list(row) for row in await db.fetchall("SELECT * FROM market.messages_old") + ]: + await db.execute( + """ + INSERT INTO market.messages( + id, + msg, + pubkey, + id_conversation, + timestamp + ) VALUES (?, ?, ?, ?, ?) + """, + ( + str(row[0]), + row[1], + row[2], + row[3], + int(row[4]), + ), + ) + + await db.execute("DROP TABLE market.messages_old") From a5d69194652eb000ebde3676105d9dc083c43216 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Mon, 27 Feb 2023 17:45:08 +0000 Subject: [PATCH 002/891] Add files via upload --- README.md | 281 ++++++++++++++++++++++++++++++++++++++++++++++ migrations.py | 302 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 583 insertions(+) create mode 100644 README.md create mode 100644 migrations.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..88e796f --- /dev/null +++ b/README.md @@ -0,0 +1,281 @@ +## Nostr Diagon Alley protocol (for resilient marketplaces) + +#### Original protocol https://github.com/lnbits/Diagon-Alley + +> The concepts around resilience in Diagon Alley helped influence the creation of the NOSTR protocol, now we get to build Diagon Alley on NOSTR! + +In Diagon Alley, `merchant` and `customer` communicate via NOSTR relays, so loss of money, product information, and reputation become far less likely if attacked. + +A `merchant` and `customer` both have a NOSTR key-pair that are used to sign notes and subscribe to events. + +#### For further information about NOSTR, see https://github.com/nostr-protocol/nostr + + +## Terms + +* `merchant` - seller of products with NOSTR key-pair +* `customer` - buyer of products with NOSTR key-pair +* `product` - item for sale by the `merchant` +* `stall` - list of products controlled by `merchant` (a `merchant` can have multiple stalls) +* `marketplace` - clientside software for searching `stalls` and purchasing `products` + +## Diagon Alley Clients + +### Merchant admin + +Where the `merchant` creates, updates and deletes `stalls` and `products`, as well as where they manage sales, payments and communication with `customers`. + +The `merchant` admin software can be purely clientside, but for `convenience` and uptime, implementations will likely have a server listening for NOSTR events. + +### Marketplace + +`Marketplace` software should be entirely clientside, either as a stand-alone app, or as a purely frontend webpage. A `customer` subscribes to different merchant NOSTR public keys, and those `merchants` `stalls` and `products` become listed and searchable. The marketplace client is like any other ecommerce site, with basket and checkout. `Marketplaces` may also wish to include a `customer` support area for direct message communication with `merchants`. + +## `Merchant` publishing/updating products (event) + +NIP-01 https://github.com/nostr-protocol/nips/blob/master/01.md uses the basic NOSTR event type. + +The `merchant` event that publishes and updates product lists + +The below json goes in `content` of NIP-01. + +Data from newer events should replace data from older events. + +`action` types (used to indicate changes): +* `update` element has changed +* `delete` element should be deleted +* `suspend` element is suspended +* `unsuspend` element is unsuspended + + +``` +{ + "name": , + "description": , + "currency": , + "action": , + "shipping": [ + { + "id": , + "zones": , + "price": , + }, + { + "id": , + "zones": , + "price": , + }, + { + "id": , + "zones": , + "price": , + } + ], + "stalls": [ + { + "id": , + "name": , + "description": , + "categories": , + "shipping": , + "action": , + "products": [ + { + "id": , + "name": , + "description": , + "categories": , + "amount": , + "price": , + "images": [ + { + "id": , + "name": , + "link": + } + ], + "action": , + }, + { + "id": , + "name": , + "description": , + "categories": , + "amount": , + "price": , + "images": [ + { + "id": , + "name": , + "link": + }, + { + "id": , + "name": , + "link": + } + ], + "action": , + }, + ] + }, + { + "id": , + "name": , + "description": , + "categories": , + "shipping": , + "action": , + "products": [ + { + "id": , + "name": , + "categories": , + "amount": , + "price": , + "images": [ + { + "id": , + "name": , + "link": + } + ], + "action": , + } + ] + } + ] +} + +``` + +As all elements are optional, an `update` `action` to a `product` `image`, may look as simple as: + +``` +{ + "stalls": [ + { + "id": , + "products": [ + { + "id": , + "images": [ + { + "id": , + "name": , + "link": + } + ], + "action": , + }, + ] + } + ] +} + +``` + + +## Checkout events + +NIP-04 https://github.com/nostr-protocol/nips/blob/master/04.md, all checkout events are encrypted + +The below json goes in `content` of NIP-04. + +### Step 1: `customer` order (event) + + +``` +{ + "id": , + "name": , + "description": , + "address": , + "message": , + "contact": [ + "nostr": , + "phone": , + "email": + ], + "items": [ + { + "id": , + "quantity": , + "message": + }, + { + "id": , + "quantity": , + "message": + }, + { + "id": , + "quantity": , + "message": + } + +} + +``` + +Merchant should verify the sum of product ids + timestamp. + +### Step 2: `merchant` request payment (event) + +Sent back from the merchant for payment. Any payment option is valid that the merchant can check. + +The below json goes in `content` of NIP-04. + +`payment_options`/`type` include: +* `url` URL to a payment page, stripe, paypal, btcpayserver, etc +* `btc` onchain bitcoin address +* `ln` bitcoin lightning invoice +* `lnurl` bitcoin lnurl-pay + +``` +{ + "id": , + "message": , + "payment_options": [ + { + "type": , + "link": + }, + { + "type": , + "link": + }, + { + "type": , + "link": + } +} + +``` + +### Step 3: `merchant` verify payment/shipped (event) + +Once payment has been received and processed. + +The below json goes in `content` of NIP-04. + +``` +{ + "id": , + "message": , + "paid": , + "shipped": , +} + +``` + +## Customer support events + +Customer support is handle over whatever communication method was specified. If communicationg via nostr, NIP-04 is used https://github.com/nostr-protocol/nips/blob/master/04.md. + +## Additional + +Standard data models can be found here here + + + diff --git a/migrations.py b/migrations.py new file mode 100644 index 0000000..f7ce7d0 --- /dev/null +++ b/migrations.py @@ -0,0 +1,302 @@ +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)" + ) + + +async def m002_add_custom_relays(db): + """ + Add custom relays to stores + """ + await db.execute("ALTER TABLE market.stalls ADD COLUMN crelays TEXT;") + + +async def m003_fiat_base_multiplier(db): + """ + Store the multiplier for fiat prices. We store the price in cents and + remember to multiply by 100 when we use it to convert to Dollars. + """ + await db.execute( + "ALTER TABLE market.stalls ADD COLUMN fiat_base_multiplier INTEGER DEFAULT 1;" + ) + + await db.execute( + "UPDATE market.stalls SET fiat_base_multiplier = 100 WHERE NOT currency = 'sat';" + ) + + +async def m004_add_privkey_to_stalls(db): + await db.execute("ALTER TABLE market.stalls ADD COLUMN privatekey TEXT") + + +async def m005_add_currency_to_zones(db): + await db.execute("ALTER TABLE market.zones ADD COLUMN stall TEXT") + await db.execute("ALTER TABLE market.zones ADD COLUMN currency TEXT DEFAULT 'sat'") + + +async def m006_delete_market_settings(db): + await db.execute("DROP TABLE market.settings") + + +async def m007_order_id_to_UUID(db): + """ + Migrate ID column type to string for UUIDs and migrate existing data + """ + + await db.execute("ALTER TABLE market.orders RENAME TO orders_old") + await db.execute( + f""" + CREATE TABLE market.orders ( + id TEXT 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 + + """ + ); + """ + ) + + for row in [ + list(row) for row in await db.fetchall("SELECT * FROM market.orders_old") + ]: + await db.execute( + """ + INSERT INTO market.orders ( + id, + wallet, + username, + pubkey, + shippingzone, + address, + email, + total, + invoiceid, + paid, + shipped, + time + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, + ( + str(row[0]), + row[1], + row[2], + row[3], + row[4], + row[5], + row[6], + row[7], + row[8], + row[9], + row[10], + int(row[11]), + ), + ) + + await db.execute("DROP TABLE market.orders_old") + + +async def m008_message_id_to_TEXT(db): + """ + Migrate ID column type to string for UUIDs and migrate existing data + """ + + await db.execute("ALTER TABLE market.messages RENAME TO messages_old") + await db.execute( + f""" + CREATE TABLE market.messages ( + id TEXT PRIMARY KEY, + msg TEXT NOT NULL, + pubkey TEXT NOT NULL, + id_conversation TEXT NOT NULL, + timestamp TIMESTAMP NOT NULL DEFAULT """ + + db.timestamp_now + + """ + ); + """ + ) + + for row in [ + list(row) for row in await db.fetchall("SELECT * FROM market.messages_old") + ]: + await db.execute( + """ + INSERT INTO market.messages( + id, + msg, + pubkey, + id_conversation, + timestamp + ) VALUES (?, ?, ?, ?, ?) + """, + ( + str(row[0]), + row[1], + row[2], + row[3], + int(row[4]), + ), + ) + + await db.execute("DROP TABLE market.messages_old") From d76f40baaffa0b67ca112da842fd5c59eabde784 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Mon, 27 Feb 2023 17:47:43 +0000 Subject: [PATCH 003/891] add .gitignore --- .gitignore | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..10a11d5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +.DS_Store +._* + +__pycache__ +*.py[cod] +*$py.class +.mypy_cache +.vscode +*-lock.json + +*.egg +*.egg-info +.coverage +.pytest_cache +.webassets-cache +htmlcov +test-reports +tests/data/*.sqlite3 + +*.swo +*.swp +*.pyo +*.pyc +*.env \ No newline at end of file From 37e1f0d3f734314d8d1882dee073258db18ee76b Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Mon, 27 Feb 2023 17:47:43 +0000 Subject: [PATCH 004/891] add .gitignore --- .gitignore | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..10a11d5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +.DS_Store +._* + +__pycache__ +*.py[cod] +*$py.class +.mypy_cache +.vscode +*-lock.json + +*.egg +*.egg-info +.coverage +.pytest_cache +.webassets-cache +htmlcov +test-reports +tests/data/*.sqlite3 + +*.swo +*.swp +*.pyo +*.pyc +*.env \ No newline at end of file From cc65c917261c7ea2d34054bf7b7adfa930a9c3e6 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Mon, 27 Feb 2023 18:13:26 +0000 Subject: [PATCH 005/891] Initial skeleton --- config.json | 0 .../stall-details/stall-details.html | 0 .../components/stall-details/stall-details.js | 15 ++++++++++ static/js/index.js | 15 ++++++++++ static/js/utils.js | 18 ++++++++++++ templates/nostrmarket/_api_docs.html | 26 +++++++++++++++++ templates/nostrmarket/index.html | 29 +++++++++++++++++++ 7 files changed, 103 insertions(+) create mode 100644 config.json create mode 100644 static/components/stall-details/stall-details.html create mode 100644 static/components/stall-details/stall-details.js create mode 100644 static/js/index.js create mode 100644 static/js/utils.js create mode 100644 templates/nostrmarket/_api_docs.html create mode 100644 templates/nostrmarket/index.html diff --git a/config.json b/config.json new file mode 100644 index 0000000..e69de29 diff --git a/static/components/stall-details/stall-details.html b/static/components/stall-details/stall-details.html new file mode 100644 index 0000000..e69de29 diff --git a/static/components/stall-details/stall-details.js b/static/components/stall-details/stall-details.js new file mode 100644 index 0000000..5b929e1 --- /dev/null +++ b/static/components/stall-details/stall-details.js @@ -0,0 +1,15 @@ +async function stallDetails(path) { + const template = await loadTemplateAsync(path) + Vue.component('stall-details', { + name: 'stall-details', + template, + + //props: ['stall-id', 'adminkey', 'inkey', 'wallet-options'], + data: function () { + return { + tab: 'info', + relay: null + } + } + }) +} diff --git a/static/js/index.js b/static/js/index.js new file mode 100644 index 0000000..b48f3b1 --- /dev/null +++ b/static/js/index.js @@ -0,0 +1,15 @@ +const stalls = async () => { + Vue.component(VueQrcode.name, VueQrcode) + + await relayDetails('static/components/stall-details/stall-details.html') + + new Vue({ + el: '#vue', + mixins: [windowMixin], + data: function () { + return {} + } + }) +} + +stalls() diff --git a/static/js/utils.js b/static/js/utils.js new file mode 100644 index 0000000..11ebc81 --- /dev/null +++ b/static/js/utils.js @@ -0,0 +1,18 @@ +function loadTemplateAsync(path) { + const result = new Promise(resolve => { + const xhttp = new XMLHttpRequest() + + xhttp.onreadystatechange = function () { + if (this.readyState == 4) { + if (this.status == 200) resolve(this.responseText) + + if (this.status == 404) resolve(`
Page not found: ${path}
`) + } + } + + xhttp.open('GET', path, true) + xhttp.send() + }) + + return result +} diff --git a/templates/nostrmarket/_api_docs.html b/templates/nostrmarket/_api_docs.html new file mode 100644 index 0000000..9ed2f47 --- /dev/null +++ b/templates/nostrmarket/_api_docs.html @@ -0,0 +1,26 @@ + + +

+ Nostr Market
+ + Created by, + motorina0 +

+
+
+ Swagger REST API Documentation +
+
diff --git a/templates/nostrmarket/index.html b/templates/nostrmarket/index.html new file mode 100644 index 0000000..86f0077 --- /dev/null +++ b/templates/nostrmarket/index.html @@ -0,0 +1,29 @@ +{% extends "base.html" %} {% from "macros.jinja" import window_vars with context +%} {% block page %} +
+
+ + section + +
+ +
+ + +
+ {{SITE_TITLE}} Nostr Market Extension +
+
+ + + {% include "nostrmarket/_api_docs.html" %} + +
+
+
+{% endblock%}{% block scripts %} {{ window_vars(user) }} + + + + +{% endblock %} From eb3d1e8f992d50355f0c2b0999f3268f765a13f8 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Mon, 27 Feb 2023 18:13:26 +0000 Subject: [PATCH 006/891] Initial skeleton --- config.json | 0 .../stall-details/stall-details.html | 0 .../components/stall-details/stall-details.js | 15 ++++++++++ static/js/index.js | 15 ++++++++++ static/js/utils.js | 18 ++++++++++++ templates/nostrmarket/_api_docs.html | 26 +++++++++++++++++ templates/nostrmarket/index.html | 29 +++++++++++++++++++ 7 files changed, 103 insertions(+) create mode 100644 config.json create mode 100644 static/components/stall-details/stall-details.html create mode 100644 static/components/stall-details/stall-details.js create mode 100644 static/js/index.js create mode 100644 static/js/utils.js create mode 100644 templates/nostrmarket/_api_docs.html create mode 100644 templates/nostrmarket/index.html diff --git a/config.json b/config.json new file mode 100644 index 0000000..e69de29 diff --git a/static/components/stall-details/stall-details.html b/static/components/stall-details/stall-details.html new file mode 100644 index 0000000..e69de29 diff --git a/static/components/stall-details/stall-details.js b/static/components/stall-details/stall-details.js new file mode 100644 index 0000000..5b929e1 --- /dev/null +++ b/static/components/stall-details/stall-details.js @@ -0,0 +1,15 @@ +async function stallDetails(path) { + const template = await loadTemplateAsync(path) + Vue.component('stall-details', { + name: 'stall-details', + template, + + //props: ['stall-id', 'adminkey', 'inkey', 'wallet-options'], + data: function () { + return { + tab: 'info', + relay: null + } + } + }) +} diff --git a/static/js/index.js b/static/js/index.js new file mode 100644 index 0000000..b48f3b1 --- /dev/null +++ b/static/js/index.js @@ -0,0 +1,15 @@ +const stalls = async () => { + Vue.component(VueQrcode.name, VueQrcode) + + await relayDetails('static/components/stall-details/stall-details.html') + + new Vue({ + el: '#vue', + mixins: [windowMixin], + data: function () { + return {} + } + }) +} + +stalls() diff --git a/static/js/utils.js b/static/js/utils.js new file mode 100644 index 0000000..11ebc81 --- /dev/null +++ b/static/js/utils.js @@ -0,0 +1,18 @@ +function loadTemplateAsync(path) { + const result = new Promise(resolve => { + const xhttp = new XMLHttpRequest() + + xhttp.onreadystatechange = function () { + if (this.readyState == 4) { + if (this.status == 200) resolve(this.responseText) + + if (this.status == 404) resolve(`
Page not found: ${path}
`) + } + } + + xhttp.open('GET', path, true) + xhttp.send() + }) + + return result +} diff --git a/templates/nostrmarket/_api_docs.html b/templates/nostrmarket/_api_docs.html new file mode 100644 index 0000000..9ed2f47 --- /dev/null +++ b/templates/nostrmarket/_api_docs.html @@ -0,0 +1,26 @@ + + +

+ Nostr Market
+ + Created by, + motorina0 +

+
+
+ Swagger REST API Documentation +
+
diff --git a/templates/nostrmarket/index.html b/templates/nostrmarket/index.html new file mode 100644 index 0000000..86f0077 --- /dev/null +++ b/templates/nostrmarket/index.html @@ -0,0 +1,29 @@ +{% extends "base.html" %} {% from "macros.jinja" import window_vars with context +%} {% block page %} +
+
+ + section + +
+ +
+ + +
+ {{SITE_TITLE}} Nostr Market Extension +
+
+ + + {% include "nostrmarket/_api_docs.html" %} + +
+
+
+{% endblock%}{% block scripts %} {{ window_vars(user) }} + + + + +{% endblock %} From 702b67ad66c6365eb6cf6d1af5fdccde02f89652 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Mon, 27 Feb 2023 18:32:13 +0000 Subject: [PATCH 007/891] More skeleton --- __init__.py | 38 +++++++ config.json | 6 + migrations.py | 302 -------------------------------------------------- views.py | 22 ++++ 4 files changed, 66 insertions(+), 302 deletions(-) create mode 100644 __init__.py create mode 100644 views.py diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..bac6100 --- /dev/null +++ b/__init__.py @@ -0,0 +1,38 @@ +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_nostrmarket") + +market_ext: APIRouter = APIRouter(prefix="/nostrmarket", tags=["nostrmarket"]) + +market_static_files = [ + { + "path": "/nostrmarket/static", + "app": StaticFiles(directory="lnbits/extensions/nostrmarket/static"), + "name": "nostrmarket_static", + } +] + + +def market_renderer(): + return template_renderer(["lnbits/extensions/nostrmarket/templates"]) + + +scheduled_tasks: List[asyncio.Task] = [] + +from .tasks import subscribe_nostrclient_ws, wait_for_paid_invoices +from .views import * # noqa +from .views_api import * # noqa + + +def nostrmarket_start(): + loop = asyncio.get_event_loop() + task1 = loop.create_task(catch_everything_and_restart(wait_for_paid_invoices)) + task2 = loop.create_task(catch_everything_and_restart(subscribe_nostrclient_ws)) + scheduled_tasks.append([task1, task2]) diff --git a/config.json b/config.json index e69de29..078ffd9 100644 --- a/config.json +++ b/config.json @@ -0,0 +1,6 @@ +{ + "name": "Nostr Market", + "short_description": "Nostr Webshop/market on LNbits", + "tile": "", + "contributors": [] +} diff --git a/migrations.py b/migrations.py index f7ce7d0..e69de29 100644 --- a/migrations.py +++ b/migrations.py @@ -1,302 +0,0 @@ -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)" - ) - - -async def m002_add_custom_relays(db): - """ - Add custom relays to stores - """ - await db.execute("ALTER TABLE market.stalls ADD COLUMN crelays TEXT;") - - -async def m003_fiat_base_multiplier(db): - """ - Store the multiplier for fiat prices. We store the price in cents and - remember to multiply by 100 when we use it to convert to Dollars. - """ - await db.execute( - "ALTER TABLE market.stalls ADD COLUMN fiat_base_multiplier INTEGER DEFAULT 1;" - ) - - await db.execute( - "UPDATE market.stalls SET fiat_base_multiplier = 100 WHERE NOT currency = 'sat';" - ) - - -async def m004_add_privkey_to_stalls(db): - await db.execute("ALTER TABLE market.stalls ADD COLUMN privatekey TEXT") - - -async def m005_add_currency_to_zones(db): - await db.execute("ALTER TABLE market.zones ADD COLUMN stall TEXT") - await db.execute("ALTER TABLE market.zones ADD COLUMN currency TEXT DEFAULT 'sat'") - - -async def m006_delete_market_settings(db): - await db.execute("DROP TABLE market.settings") - - -async def m007_order_id_to_UUID(db): - """ - Migrate ID column type to string for UUIDs and migrate existing data - """ - - await db.execute("ALTER TABLE market.orders RENAME TO orders_old") - await db.execute( - f""" - CREATE TABLE market.orders ( - id TEXT 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 - + """ - ); - """ - ) - - for row in [ - list(row) for row in await db.fetchall("SELECT * FROM market.orders_old") - ]: - await db.execute( - """ - INSERT INTO market.orders ( - id, - wallet, - username, - pubkey, - shippingzone, - address, - email, - total, - invoiceid, - paid, - shipped, - time - ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - """, - ( - str(row[0]), - row[1], - row[2], - row[3], - row[4], - row[5], - row[6], - row[7], - row[8], - row[9], - row[10], - int(row[11]), - ), - ) - - await db.execute("DROP TABLE market.orders_old") - - -async def m008_message_id_to_TEXT(db): - """ - Migrate ID column type to string for UUIDs and migrate existing data - """ - - await db.execute("ALTER TABLE market.messages RENAME TO messages_old") - await db.execute( - f""" - CREATE TABLE market.messages ( - id TEXT PRIMARY KEY, - msg TEXT NOT NULL, - pubkey TEXT NOT NULL, - id_conversation TEXT NOT NULL, - timestamp TIMESTAMP NOT NULL DEFAULT """ - + db.timestamp_now - + """ - ); - """ - ) - - for row in [ - list(row) for row in await db.fetchall("SELECT * FROM market.messages_old") - ]: - await db.execute( - """ - INSERT INTO market.messages( - id, - msg, - pubkey, - id_conversation, - timestamp - ) VALUES (?, ?, ?, ?, ?) - """, - ( - str(row[0]), - row[1], - row[2], - row[3], - int(row[4]), - ), - ) - - await db.execute("DROP TABLE market.messages_old") diff --git a/views.py b/views.py new file mode 100644 index 0000000..568d515 --- /dev/null +++ b/views.py @@ -0,0 +1,22 @@ +import json +from http import HTTPStatus + +from fastapi import Depends, Request, HTMLResponse +from fastapi.templating import Jinja2Templates +from loguru import logger + +from lnbits.core.models import User +from lnbits.decorators import check_user_exists + +from . import nostrmarket_ext, nostrmarket_renderer + + +templates = Jinja2Templates(directory="templates") + + +@nostrmarket_ext.get("/", response_class=HTMLResponse) +async def index(request: Request, user: User = Depends(check_user_exists)): + return nostrmarket_renderer().TemplateResponse( + "nostrmarket/index.html", + {"request": request, "user": user.dict()}, + ) From 5b23b0054b29bed8044d17dd313bbd345068668e Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Mon, 27 Feb 2023 18:32:13 +0000 Subject: [PATCH 008/891] More skeleton --- __init__.py | 38 +++++++ config.json | 6 + migrations.py | 302 -------------------------------------------------- views.py | 22 ++++ 4 files changed, 66 insertions(+), 302 deletions(-) create mode 100644 __init__.py create mode 100644 views.py diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..bac6100 --- /dev/null +++ b/__init__.py @@ -0,0 +1,38 @@ +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_nostrmarket") + +market_ext: APIRouter = APIRouter(prefix="/nostrmarket", tags=["nostrmarket"]) + +market_static_files = [ + { + "path": "/nostrmarket/static", + "app": StaticFiles(directory="lnbits/extensions/nostrmarket/static"), + "name": "nostrmarket_static", + } +] + + +def market_renderer(): + return template_renderer(["lnbits/extensions/nostrmarket/templates"]) + + +scheduled_tasks: List[asyncio.Task] = [] + +from .tasks import subscribe_nostrclient_ws, wait_for_paid_invoices +from .views import * # noqa +from .views_api import * # noqa + + +def nostrmarket_start(): + loop = asyncio.get_event_loop() + task1 = loop.create_task(catch_everything_and_restart(wait_for_paid_invoices)) + task2 = loop.create_task(catch_everything_and_restart(subscribe_nostrclient_ws)) + scheduled_tasks.append([task1, task2]) diff --git a/config.json b/config.json index e69de29..078ffd9 100644 --- a/config.json +++ b/config.json @@ -0,0 +1,6 @@ +{ + "name": "Nostr Market", + "short_description": "Nostr Webshop/market on LNbits", + "tile": "", + "contributors": [] +} diff --git a/migrations.py b/migrations.py index f7ce7d0..e69de29 100644 --- a/migrations.py +++ b/migrations.py @@ -1,302 +0,0 @@ -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)" - ) - - -async def m002_add_custom_relays(db): - """ - Add custom relays to stores - """ - await db.execute("ALTER TABLE market.stalls ADD COLUMN crelays TEXT;") - - -async def m003_fiat_base_multiplier(db): - """ - Store the multiplier for fiat prices. We store the price in cents and - remember to multiply by 100 when we use it to convert to Dollars. - """ - await db.execute( - "ALTER TABLE market.stalls ADD COLUMN fiat_base_multiplier INTEGER DEFAULT 1;" - ) - - await db.execute( - "UPDATE market.stalls SET fiat_base_multiplier = 100 WHERE NOT currency = 'sat';" - ) - - -async def m004_add_privkey_to_stalls(db): - await db.execute("ALTER TABLE market.stalls ADD COLUMN privatekey TEXT") - - -async def m005_add_currency_to_zones(db): - await db.execute("ALTER TABLE market.zones ADD COLUMN stall TEXT") - await db.execute("ALTER TABLE market.zones ADD COLUMN currency TEXT DEFAULT 'sat'") - - -async def m006_delete_market_settings(db): - await db.execute("DROP TABLE market.settings") - - -async def m007_order_id_to_UUID(db): - """ - Migrate ID column type to string for UUIDs and migrate existing data - """ - - await db.execute("ALTER TABLE market.orders RENAME TO orders_old") - await db.execute( - f""" - CREATE TABLE market.orders ( - id TEXT 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 - + """ - ); - """ - ) - - for row in [ - list(row) for row in await db.fetchall("SELECT * FROM market.orders_old") - ]: - await db.execute( - """ - INSERT INTO market.orders ( - id, - wallet, - username, - pubkey, - shippingzone, - address, - email, - total, - invoiceid, - paid, - shipped, - time - ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - """, - ( - str(row[0]), - row[1], - row[2], - row[3], - row[4], - row[5], - row[6], - row[7], - row[8], - row[9], - row[10], - int(row[11]), - ), - ) - - await db.execute("DROP TABLE market.orders_old") - - -async def m008_message_id_to_TEXT(db): - """ - Migrate ID column type to string for UUIDs and migrate existing data - """ - - await db.execute("ALTER TABLE market.messages RENAME TO messages_old") - await db.execute( - f""" - CREATE TABLE market.messages ( - id TEXT PRIMARY KEY, - msg TEXT NOT NULL, - pubkey TEXT NOT NULL, - id_conversation TEXT NOT NULL, - timestamp TIMESTAMP NOT NULL DEFAULT """ - + db.timestamp_now - + """ - ); - """ - ) - - for row in [ - list(row) for row in await db.fetchall("SELECT * FROM market.messages_old") - ]: - await db.execute( - """ - INSERT INTO market.messages( - id, - msg, - pubkey, - id_conversation, - timestamp - ) VALUES (?, ?, ?, ?, ?) - """, - ( - str(row[0]), - row[1], - row[2], - row[3], - int(row[4]), - ), - ) - - await db.execute("DROP TABLE market.messages_old") diff --git a/views.py b/views.py new file mode 100644 index 0000000..568d515 --- /dev/null +++ b/views.py @@ -0,0 +1,22 @@ +import json +from http import HTTPStatus + +from fastapi import Depends, Request, HTMLResponse +from fastapi.templating import Jinja2Templates +from loguru import logger + +from lnbits.core.models import User +from lnbits.decorators import check_user_exists + +from . import nostrmarket_ext, nostrmarket_renderer + + +templates = Jinja2Templates(directory="templates") + + +@nostrmarket_ext.get("/", response_class=HTMLResponse) +async def index(request: Request, user: User = Depends(check_user_exists)): + return nostrmarket_renderer().TemplateResponse( + "nostrmarket/index.html", + {"request": request, "user": user.dict()}, + ) From 94ce670866e7d4ce197ffbc79ad2cb251d1e824c Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Mon, 27 Feb 2023 18:34:34 +0000 Subject: [PATCH 009/891] naming fix --- __init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/__init__.py b/__init__.py index bac6100..6757f5a 100644 --- a/__init__.py +++ b/__init__.py @@ -1,4 +1,5 @@ import asyncio +from typing import List from fastapi import APIRouter from starlette.staticfiles import StaticFiles @@ -9,9 +10,9 @@ from lnbits.tasks import catch_everything_and_restart db = Database("ext_nostrmarket") -market_ext: APIRouter = APIRouter(prefix="/nostrmarket", tags=["nostrmarket"]) +nostrmarket_ext: APIRouter = APIRouter(prefix="/nostrmarket", tags=["nostrmarket"]) -market_static_files = [ +nostrmarket_static_files = [ { "path": "/nostrmarket/static", "app": StaticFiles(directory="lnbits/extensions/nostrmarket/static"), From 03825d91448cc8f20fd9b2738953e3600bda0f35 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Mon, 27 Feb 2023 18:34:34 +0000 Subject: [PATCH 010/891] naming fix --- __init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/__init__.py b/__init__.py index bac6100..6757f5a 100644 --- a/__init__.py +++ b/__init__.py @@ -1,4 +1,5 @@ import asyncio +from typing import List from fastapi import APIRouter from starlette.staticfiles import StaticFiles @@ -9,9 +10,9 @@ from lnbits.tasks import catch_everything_and_restart db = Database("ext_nostrmarket") -market_ext: APIRouter = APIRouter(prefix="/nostrmarket", tags=["nostrmarket"]) +nostrmarket_ext: APIRouter = APIRouter(prefix="/nostrmarket", tags=["nostrmarket"]) -market_static_files = [ +nostrmarket_static_files = [ { "path": "/nostrmarket/static", "app": StaticFiles(directory="lnbits/extensions/nostrmarket/static"), From 9b6b1e87b91f41fa69ea7b49dee3c5b8a89adf8e Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Tue, 28 Feb 2023 09:19:27 +0200 Subject: [PATCH 011/891] chore: get the extension started --- __init__.py | 2 +- tasks.py | 33 +++++++++++++++++++++++++++++++++ views.py | 3 ++- views_api.py | 0 4 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 tasks.py create mode 100644 views_api.py diff --git a/__init__.py b/__init__.py index 6757f5a..9e188f2 100644 --- a/__init__.py +++ b/__init__.py @@ -21,7 +21,7 @@ nostrmarket_static_files = [ ] -def market_renderer(): +def nostrmarket_renderer(): return template_renderer(["lnbits/extensions/nostrmarket/templates"]) diff --git a/tasks.py b/tasks.py new file mode 100644 index 0000000..c2a1abf --- /dev/null +++ b/tasks.py @@ -0,0 +1,33 @@ +import asyncio +import json +import threading + +import httpx +import websocket +from loguru import logger + +from lnbits.core.models import Payment +from lnbits.helpers import url_for +from lnbits.tasks import register_invoice_listener + + + +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 payment.extra.get("tag") != "market": + return + + print("### on_invoice_paid") + + +async def subscribe_nostrclient_ws(): + print("### subscribe_nostrclient_ws") + diff --git a/views.py b/views.py index 568d515..d836f29 100644 --- a/views.py +++ b/views.py @@ -1,7 +1,8 @@ import json from http import HTTPStatus -from fastapi import Depends, Request, HTMLResponse +from fastapi import Depends, Request +from starlette.responses import HTMLResponse from fastapi.templating import Jinja2Templates from loguru import logger diff --git a/views_api.py b/views_api.py new file mode 100644 index 0000000..e69de29 From 114c895732a7cb8cc5f6316ec4fe4adb58b1de98 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Tue, 28 Feb 2023 09:19:27 +0200 Subject: [PATCH 012/891] chore: get the extension started --- __init__.py | 2 +- tasks.py | 33 +++++++++++++++++++++++++++++++++ views.py | 3 ++- views_api.py | 0 4 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 tasks.py create mode 100644 views_api.py diff --git a/__init__.py b/__init__.py index 6757f5a..9e188f2 100644 --- a/__init__.py +++ b/__init__.py @@ -21,7 +21,7 @@ nostrmarket_static_files = [ ] -def market_renderer(): +def nostrmarket_renderer(): return template_renderer(["lnbits/extensions/nostrmarket/templates"]) diff --git a/tasks.py b/tasks.py new file mode 100644 index 0000000..c2a1abf --- /dev/null +++ b/tasks.py @@ -0,0 +1,33 @@ +import asyncio +import json +import threading + +import httpx +import websocket +from loguru import logger + +from lnbits.core.models import Payment +from lnbits.helpers import url_for +from lnbits.tasks import register_invoice_listener + + + +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 payment.extra.get("tag") != "market": + return + + print("### on_invoice_paid") + + +async def subscribe_nostrclient_ws(): + print("### subscribe_nostrclient_ws") + diff --git a/views.py b/views.py index 568d515..d836f29 100644 --- a/views.py +++ b/views.py @@ -1,7 +1,8 @@ import json from http import HTTPStatus -from fastapi import Depends, Request, HTMLResponse +from fastapi import Depends, Request +from starlette.responses import HTMLResponse from fastapi.templating import Jinja2Templates from loguru import logger diff --git a/views_api.py b/views_api.py new file mode 100644 index 0000000..e69de29 From 5484560435fc73ccdb625c31f97d9a6b916917a4 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Tue, 28 Feb 2023 09:44:46 +0200 Subject: [PATCH 013/891] chore: code format --- README.md | 41 ++++++++++++++++++----------------------- tasks.py | 2 -- views.py | 3 +-- 3 files changed, 19 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 88e796f..1cd1e04 100644 --- a/README.md +++ b/README.md @@ -6,24 +6,23 @@ In Diagon Alley, `merchant` and `customer` communicate via NOSTR relays, so loss of money, product information, and reputation become far less likely if attacked. -A `merchant` and `customer` both have a NOSTR key-pair that are used to sign notes and subscribe to events. +A `merchant` and `customer` both have a NOSTR key-pair that are used to sign notes and subscribe to events. #### For further information about NOSTR, see https://github.com/nostr-protocol/nostr - ## Terms -* `merchant` - seller of products with NOSTR key-pair -* `customer` - buyer of products with NOSTR key-pair -* `product` - item for sale by the `merchant` -* `stall` - list of products controlled by `merchant` (a `merchant` can have multiple stalls) -* `marketplace` - clientside software for searching `stalls` and purchasing `products` +- `merchant` - seller of products with NOSTR key-pair +- `customer` - buyer of products with NOSTR key-pair +- `product` - item for sale by the `merchant` +- `stall` - list of products controlled by `merchant` (a `merchant` can have multiple stalls) +- `marketplace` - clientside software for searching `stalls` and purchasing `products` ## Diagon Alley Clients ### Merchant admin -Where the `merchant` creates, updates and deletes `stalls` and `products`, as well as where they manage sales, payments and communication with `customers`. +Where the `merchant` creates, updates and deletes `stalls` and `products`, as well as where they manage sales, payments and communication with `customers`. The `merchant` admin software can be purely clientside, but for `convenience` and uptime, implementations will likely have a server listening for NOSTR events. @@ -39,14 +38,14 @@ The `merchant` event that publishes and updates product lists The below json goes in `content` of NIP-01. -Data from newer events should replace data from older events. +Data from newer events should replace data from older events. `action` types (used to indicate changes): -* `update` element has changed -* `delete` element should be deleted -* `suspend` element is suspended -* `unsuspend` element is unsuspended +- `update` element has changed +- `delete` element should be deleted +- `suspend` element is suspended +- `unsuspend` element is unsuspended ``` { @@ -175,7 +174,6 @@ As all elements are optional, an `update` `action` to a `product` `image`, may l ``` - ## Checkout events NIP-04 https://github.com/nostr-protocol/nips/blob/master/04.md, all checkout events are encrypted @@ -184,7 +182,6 @@ The below json goes in `content` of NIP-04. ### Step 1: `customer` order (event) - ``` { "id": , @@ -213,7 +210,7 @@ The below json goes in `content` of NIP-04. "quantity": , "message": } - + } ``` @@ -227,10 +224,11 @@ Sent back from the merchant for payment. Any payment option is valid that the me The below json goes in `content` of NIP-04. `payment_options`/`type` include: -* `url` URL to a payment page, stripe, paypal, btcpayserver, etc -* `btc` onchain bitcoin address -* `ln` bitcoin lightning invoice -* `lnurl` bitcoin lnurl-pay + +- `url` URL to a payment page, stripe, paypal, btcpayserver, etc +- `btc` onchain bitcoin address +- `ln` bitcoin lightning invoice +- `lnurl` bitcoin lnurl-pay ``` { @@ -276,6 +274,3 @@ Customer support is handle over whatever communication method was specified. If ## Additional Standard data models can be found here here - - - diff --git a/tasks.py b/tasks.py index c2a1abf..cbf1b74 100644 --- a/tasks.py +++ b/tasks.py @@ -11,7 +11,6 @@ from lnbits.helpers import url_for from lnbits.tasks import register_invoice_listener - async def wait_for_paid_invoices(): invoice_queue = asyncio.Queue() register_invoice_listener(invoice_queue) @@ -30,4 +29,3 @@ async def on_invoice_paid(payment: Payment) -> None: async def subscribe_nostrclient_ws(): print("### subscribe_nostrclient_ws") - diff --git a/views.py b/views.py index d836f29..ca8e1f7 100644 --- a/views.py +++ b/views.py @@ -2,16 +2,15 @@ import json from http import HTTPStatus from fastapi import Depends, Request -from starlette.responses import HTMLResponse from fastapi.templating import Jinja2Templates from loguru import logger +from starlette.responses import HTMLResponse from lnbits.core.models import User from lnbits.decorators import check_user_exists from . import nostrmarket_ext, nostrmarket_renderer - templates = Jinja2Templates(directory="templates") From e15cf305780000fa1de5b24a315e4700f33be54d Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Tue, 28 Feb 2023 09:44:46 +0200 Subject: [PATCH 014/891] chore: code format --- README.md | 41 ++++++++++++++++++----------------------- tasks.py | 2 -- views.py | 3 +-- 3 files changed, 19 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 88e796f..1cd1e04 100644 --- a/README.md +++ b/README.md @@ -6,24 +6,23 @@ In Diagon Alley, `merchant` and `customer` communicate via NOSTR relays, so loss of money, product information, and reputation become far less likely if attacked. -A `merchant` and `customer` both have a NOSTR key-pair that are used to sign notes and subscribe to events. +A `merchant` and `customer` both have a NOSTR key-pair that are used to sign notes and subscribe to events. #### For further information about NOSTR, see https://github.com/nostr-protocol/nostr - ## Terms -* `merchant` - seller of products with NOSTR key-pair -* `customer` - buyer of products with NOSTR key-pair -* `product` - item for sale by the `merchant` -* `stall` - list of products controlled by `merchant` (a `merchant` can have multiple stalls) -* `marketplace` - clientside software for searching `stalls` and purchasing `products` +- `merchant` - seller of products with NOSTR key-pair +- `customer` - buyer of products with NOSTR key-pair +- `product` - item for sale by the `merchant` +- `stall` - list of products controlled by `merchant` (a `merchant` can have multiple stalls) +- `marketplace` - clientside software for searching `stalls` and purchasing `products` ## Diagon Alley Clients ### Merchant admin -Where the `merchant` creates, updates and deletes `stalls` and `products`, as well as where they manage sales, payments and communication with `customers`. +Where the `merchant` creates, updates and deletes `stalls` and `products`, as well as where they manage sales, payments and communication with `customers`. The `merchant` admin software can be purely clientside, but for `convenience` and uptime, implementations will likely have a server listening for NOSTR events. @@ -39,14 +38,14 @@ The `merchant` event that publishes and updates product lists The below json goes in `content` of NIP-01. -Data from newer events should replace data from older events. +Data from newer events should replace data from older events. `action` types (used to indicate changes): -* `update` element has changed -* `delete` element should be deleted -* `suspend` element is suspended -* `unsuspend` element is unsuspended +- `update` element has changed +- `delete` element should be deleted +- `suspend` element is suspended +- `unsuspend` element is unsuspended ``` { @@ -175,7 +174,6 @@ As all elements are optional, an `update` `action` to a `product` `image`, may l ``` - ## Checkout events NIP-04 https://github.com/nostr-protocol/nips/blob/master/04.md, all checkout events are encrypted @@ -184,7 +182,6 @@ The below json goes in `content` of NIP-04. ### Step 1: `customer` order (event) - ``` { "id": , @@ -213,7 +210,7 @@ The below json goes in `content` of NIP-04. "quantity": , "message": } - + } ``` @@ -227,10 +224,11 @@ Sent back from the merchant for payment. Any payment option is valid that the me The below json goes in `content` of NIP-04. `payment_options`/`type` include: -* `url` URL to a payment page, stripe, paypal, btcpayserver, etc -* `btc` onchain bitcoin address -* `ln` bitcoin lightning invoice -* `lnurl` bitcoin lnurl-pay + +- `url` URL to a payment page, stripe, paypal, btcpayserver, etc +- `btc` onchain bitcoin address +- `ln` bitcoin lightning invoice +- `lnurl` bitcoin lnurl-pay ``` { @@ -276,6 +274,3 @@ Customer support is handle over whatever communication method was specified. If ## Additional Standard data models can be found here here - - - diff --git a/tasks.py b/tasks.py index c2a1abf..cbf1b74 100644 --- a/tasks.py +++ b/tasks.py @@ -11,7 +11,6 @@ from lnbits.helpers import url_for from lnbits.tasks import register_invoice_listener - async def wait_for_paid_invoices(): invoice_queue = asyncio.Queue() register_invoice_listener(invoice_queue) @@ -30,4 +29,3 @@ async def on_invoice_paid(payment: Payment) -> None: async def subscribe_nostrclient_ws(): print("### subscribe_nostrclient_ws") - diff --git a/views.py b/views.py index d836f29..ca8e1f7 100644 --- a/views.py +++ b/views.py @@ -2,16 +2,15 @@ import json from http import HTTPStatus from fastapi import Depends, Request -from starlette.responses import HTMLResponse from fastapi.templating import Jinja2Templates from loguru import logger +from starlette.responses import HTMLResponse from lnbits.core.models import User from lnbits.decorators import check_user_exists from . import nostrmarket_ext, nostrmarket_renderer - templates = Jinja2Templates(directory="templates") From 8a459acf80028493190984d41ad898dfadb41f06 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Tue, 28 Feb 2023 09:45:02 +0200 Subject: [PATCH 015/891] chore: add images --- static/images/bitcoin-shop.png | Bin 0 -> 6034 bytes static/images/placeholder.png | Bin 0 -> 2840 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 static/images/bitcoin-shop.png create mode 100644 static/images/placeholder.png diff --git a/static/images/bitcoin-shop.png b/static/images/bitcoin-shop.png new file mode 100644 index 0000000000000000000000000000000000000000..debffbb28586737964b97aa788242ebf68ded85e GIT binary patch literal 6034 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 G2~7YIWw#>$ literal 0 HcmV?d00001 From 6cb3e4e7a0dab8be282a5eef6786e3a485681777 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Tue, 28 Feb 2023 09:45:02 +0200 Subject: [PATCH 016/891] chore: add images --- static/images/bitcoin-shop.png | Bin 0 -> 6034 bytes static/images/placeholder.png | Bin 0 -> 2840 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 static/images/bitcoin-shop.png create mode 100644 static/images/placeholder.png diff --git a/static/images/bitcoin-shop.png b/static/images/bitcoin-shop.png new file mode 100644 index 0000000000000000000000000000000000000000..debffbb28586737964b97aa788242ebf68ded85e GIT binary patch literal 6034 zcmeAS@N?(olHy`uVBq!ia0y~yU}ykg4mJh`hQoG=rx_R+TQZ%U13aCb6$*;-(=u~X z85lGs)=sqbI2<6->VLUQNtE5V!y;vcpO}lQ%vLVf6#`kUVX{7Pzu0}HCW(lW&OE+oHhYI=UcZ)0D+c{)}%*b;;#(1}2O^K0W zYVqoz@7his4lN9xiQ6pIPiwMVyKY+=eD`Q&Si$}ISC>D{WouD+6qy#q_mS=6mu1OC z_teEAJuh~g?>w%vOW~vP_0akEEyQibDwbuWUiy4QX=BKngsshyCogp_??|4N7qD-_ z*T~0vjz+G(67hQ8pVo_eHf}mo8gi&vZO zwa!SwQY@gA@95{|{|DdSyw!S=(_V8H!>+iP@5wAL?y=4)z5ds(?Cvv8fjYKzJ=^x* zVo0;|T)%zm4Z#chd1GR>o0jtLxWmXc-{)wMji!&chDgJA<6p~U3<|ivtovDg?N1#8%lD#cMU~m&3=9lxN#5=*4F5rJ!QSPQ z85kHi3p^r=85s1GL71^(seKs(0|R@Br>`sfb4C^s9lh@OpbZQR43Z_T5hc#~xw)x% zB@E6*sfi`2DGKG8B^e6tp1uL$jeOz^45C?{E{-7;x8BaJE{M5Wd))r{z1{k6*WL4v zF8VVmRr<79i=oJEiw4esprA`ZPC-tyP6{+#o^~No$Y}1Q6r)VfSw5eegeGlS>N9KG za`$wL0|@4t06a{XENB13ecOiWyCN`|Y#qVt1coZM0!-G`#C8rXP2P*Zj_bv*!2DTn&%+_4Vy7 z`dJ(>Ygu4$uybgrsNMYhLvH$eJ{+1-*k~8}etyoM)$4Y3-MST(`iJ{~_KzC7Q>RaV ztgu-Uq*+x}<)Jotru<){52DGd4C^m1^A$c8Zu@^O{{yB*hGm)0Op+=qE&1A$-BMrg zI``-2=bKwHFSnH6*jb!@?%{zGSAYMlv(LW1F7@-Xv!~wf_wG9W*yit-%Oz!HX?vfS z?XG;Wuzkg- ziw7Hz%bl*ue|lKASTDSArte5Q!zLzhQ#`?aGOJVp?V<)B;V`0nSC7!P{A=@LIA?A@}JJW+8 zdASf_4TdQq3|?FeODzSbJpKL8TGe@G(z8vuv;Y08Yin!cGK)|VO01}`cwYUnVve$V zpMi-12x8FCWSFAoxKigqO;!K0z}VQc({!V`{FsG1S#EF7 zPyhe#Z{+)*f9u}u{T}zHuHG?~OL4{e_4*l#{Y)SYi5n6Rvt3&sAOE4;d7;3;1Os&j zUS3|C9}k+ZT)QR~vrb^qf~_)K4WC06^4i_3_6dJiy-~&gD1#P9NDzYt$j+2mYHf)H z8y0t;yH^)9t@5O4%Ea48+1Ew1UVrQ4Re4ZnrM*6b&*>xsgJbb_zby9VZ@c)lOuS5| z$n%|!YRTMJudHUxnziG*{=XjU{5uU(iq5THXO?#FwQgM1+SY#CU$t)0Wr@6B8gh#@ z^L4IOe^IE_-M_f>e)gZcyZA4B>W^QrY)^`xvw<>$&h514@_a?tukZ6sIBqA`(T>(dSJq=y6qVVC2Wh)ATK5cdul(01ooObi8+U;%s?osyQR*lD{{PYk&wb zJD5yzXZ;OO)zkJ2YTo$o-WG*zm!>aSXENn&?74-Tl!^$6wFG_Vxo=g1 zpSAKgOqo>w@8x=nuirkFOxI+Yb-Vjn$->Ob|2?+rc^o+$xjS4}XS&IhlTAXLiY#^E zZu;Vn1vp}zp9ISLGM{}Y!Jhi)p4tfy$H?Ab{^F;9@;-z)KKUYbFr?}F?c1KS#9Sr6 zu3NL_$+z42{_mx?^`3rOb^e(k7n1{r<8A5u1ir7!Lba8$D_LC-x?g- z%aOp|F8Su2SANCegqN3=KL2<5S@jb}u3e}3>vzsgxX+oQHSgWOxp^jsXHNgfXk%4T zI`J7}j30CRWozxn31>D5?qaN}s(-=B#S|ELL6FB!*<)@!i-z^zGSpL zKPS46-D+V$RsM=ES!eWT&y9)X=$&ykli^&`ol+w|o5%{Epm3pt;^YT8kwHm(^B%QM zyT&cvuzdU86P@q$QfwyK-F#HCP}0w3-aGls=G4CZYRnI=M~0kSW9#+7O7_Q|c|7Iw zohHmV=#$AZ|2F5bC5Z|m5BOz|TVA?aoXBKgX5;WErF_@JmcHY!-9?P9>pDiM`EYF$ zXxgOI%=Uj`WH!gb*N+Q*qHbj*-b_;4XvDpG&z<_WOOEJIRk(HeWz24NqhH{v?o)u{pWcXWG!*M&Cz%vhQqMC_?Z)P?D-i-O)9ImbOkKh^}r&* z@uo!fGgXV-^5!%C?ydRRdOyB`;f?JNwu|$&PuN%x^&})Fsx8f;GrEkiH(9vUQ|r99t|$LhhM)PN0x=We;71m7v;DbLPTNX#ua4P#OTaL?kG)Y+^Txb;A{(Ea zvAH~pP5rw5+%1o{=L)`lnH2x~BR6})NAS0!G>+;lpcj7r(F83Df5SXR0A~fgrxl;^XcbihDDxZ0krKITL*`czQr+*s5 z%vpgGcZsD0mKye6P2W3PMpot4?V}gZt>b2HNRQ?dw!S#?>A!C8E{)D+=7HS8to4(=G<^*3%kOshRUb+EH@Sy z<$2b)&RetK+m8vZMQyfW9J64Wxb&QeXK|Y2Y=*pumx~wr z%r+6Zbz|Ag&wCbpk2-c{1>5q&8a0zbqHd&V^{P8J{|b`MZR2zB^1MG|uJr?rf{4zW zW}ebQ7r(GO=W%Ynh~*SBDC> zwpJ`B-#0d6n_f>rvv-mKPffSHITg{&l63XPB~xYot*hh* z{JnijXHy(`-&{JN=zFZ`N4dg%1M;`rm^&?|AI#?|KEk}UGFBQ+>+v2yJnKRU5Ra7{>3}&4{n#=ZChNIm}YYP z#s!9p8yWQ8Xet(DOYvm}rl@XM{BVx#rpF;O)=RW>+ z$;ZITq(in};qA)3-O<87T6-taZQ19$g=Wl4|w z7Z~$utLCY+%{;)x+QHU#z-!*&+c*AHxU27fJI7xo(lGahyyvdUq;TDS8*%U7=@ULo zdlbE+N3>h~94AA`vGWsT`5X4c&6+(sa(CI=BmYcPkJdFgJANxINn$tJ=4={#BsoxK zSKH|e2euh5>CKCNv8{7jPjVZxihH{xx6j`9x%mbMI}?THFYof2FT7s3rL2nY-iqh`HvIL_jIS?i3BR>`rrk8Nj~OiO%Mv*ZSVUe*`Kip%G%@a+*d@%#XHc|hwu^~P zsw}&cX54$fptu*Bv*QoKyT=biK29G5-+HP1BsV>A1f4K6|N3q`y+kc>+SRL9KQ*$;O<22jtx@oLIU6N!vw9b<_?dJ5%$>Q%Y~L;WkGo#} z{8n6ZPIaM6P>AkAFQ<(x&n-{ydAf4?(_`VkpX&PWo#QThWXgN-=~I_qExf#La`5$w z;kpZ*n(7zzDLt->D%~6N_v7zWlX(+1FLY5-oF`SM)vMQZ# z6b~U_EXP$~w@5AqvAN4Yp zJes!qg1C-qOziyYQzq8lwq3L1tz1oQzF_|C(+}3y9eVLXBKe`fv`j%hwoSG@hYx<8 z9)B$63eP;&l>h(t7asM>s^6mgdh$fcFs+za{;OXr*8O3SODH-v=ZvL*k748!J*B|i z3|Zn2_P;xvlDAK4cBbre-KNTJjomNLdH!dVy1wqKX=HAiMDb^lZ{<(yJ+}J4b9lKX zET--$Yw-2*XZ{(lW;`^}t@%+sU4p^urKQ`%c*X+H*GyKGv5WPx{JtZK&z$3DDQr+K|_Ge!ii}NyUJIuQ7 z%iVSJ_J1|%ii!`N#-4P)g5gke=Jk^G^G{FLo%wawQ>#^W*^@O+Z`z>4t(khwP|t~D zU)lTPH~+mcE-5WvvUhv!Y4<5#i%-6MwrSZ@6SFJlvP^SsAKSEMrLBXG*yAIe!X+go z1}0N(uMMB}W9Ig6{TH6=2Oc|n{GWID$M4a0&8g?_X(mof735*d%+8)I)exZJ;_lAw z-Y1iokdW}FXTMC|ll}7h`j5@dbI;G%vtXNZrGKhLfm!UiPty61``7PeioKjB+VJpU zVpLSrGI^$5yLV^i<}O{ivh&m_udJ-BO>K=ouTFha@g?z{mVWw!-r5qG*9ZFCo*g=w zU#!5&uy)6kXH!@IxU>9USAT7Rvftqup^w>_7|Y(@n|ql#D$TR<%~O~*fy~+Uf)}9Du1s+l&{@xwQYUT`}_Or z?cctAYsq-R;$`9;?l(6#x3{&i)!4~@etzEldhz>`mtS}McyxFB>Ur}oIzDR(S}9Un zTf67WCGRcUwiy{1ObER?XP>4B*T($&c6aj3cdgr;d3l+pqJS3PjAE3>#u3N>|Vv!e?)K3OTE10$;v-nc%hbLxs(h>C=NlLRyw(uDX8SF>+dK<}6{``rF%bWh@F5 zroXno{PHrx4=+8ANheioZEYi_wO)RCbZy-PhOGr(azadga=+N8f)7Iz5?_7E<*+^|sl; zgb5EHCYHUwclO`k-0By z_2c^wZ(hGXapJ_OQ>U(6xpMW&l`EGoU%hhm{o8l%-n_ka^Va>l_fDQXdFITS%azA)?-@bkN^y#;6 z-aLNv_`><~Cr_Nbee3pv`wyN!efH+n>x&mIT)cSk^{ZDmu3x`=>GHX=XWzblbLaM* zXHTD?k@bJ>bOSf*^eDdh=sgox! zUby(+-u)|=uRMDA@ZQ~fr%#=J{p$6rm#^NwdHep|`-cx6oIP{q|NsBh!RwbZFfcGC zdAqx0oX+{8#K6G7UgGKN%Kn%~oKs2Q;X9oK1_q9;o-U3d6}R5r{at>!MB>218v(Z* z*d8xRo+&!v?9Rx!(=9(XiKi^8a8x(a&$0UW>HmLrKg*(oEt!|L{G9f!vd;hh^v$0w z7v|1#5o8$7z-L#wl<^bE`urA6AAw~Cg3AmXXB={wafl_6OSpyekcb*b@C=UN845m$ zN6QYSjI8Ecsmk9=mO^4=Kmsb5d zbF=vQ{MvO}1-OOP6wh#XpFG^W`P-$>8x0f-X3TvaZ}IEY&7pHuiy%g$d#|EK4#=M;8o**WjY-nwsd3>N+~2%pR-?8hPe zitqE(cMCXg8YVjJO)Q(n-X9Supms&-*=pCcJ=3^^vv`UZ-p489O5& zdPe@9$O12K5No~I+)vXC7OpdRe0EXrt>Qy1vkuKU&DCESJtH7^hIm*1O(C@_BF|<% zO1=e>x}#&5?l&XAe}-qL<$Dg{SDc&A%vpV?CF+pR=`)skJ}aL0%$z#o^E!it^9**Y zKHH=2vqJsMckeTut(?y7k*~f@mFKZ_n_;lc_}SJ+$?G_Tv$!^2*0V{A1=;no-F115 z2scP9EsBz7}rR^l6EFIM05*i1en&S0WX*QJ$@x z2JQUDdL_M%Vec+FoesPrqIPIaLHy0IiKjo=>0Z4i-R`cl?2yZh!|gNA@yl(Dc&B{x z^8b0s`5eNDeDZ0^azA=nmORN{^1u3EZlcl|u0%5{=6A^-Zk{d5jIH{x?Q1bquzTn8 z)849=e?2-_=)KZFFs;=B5Ot=_D&=cd}dU;00O z^9C7i;lwb`%@-r~UVXiF|DJ$-Yu3xj-37T&EN$_|57#c{n{1ChyvO%WT}6$#sM?`5 z5^0MKzkmG{^l8Dll&74AxQpk+s)T{TW>x;|9=0(M8)tr89+XmF4yMEB`FI8#05VI3{t~@7?>i zx8Ev>pTSYAlUSA~_J7NBPJ3?Q#6G(-&8w@wug>}9uH(~U`N$yu-M^Wkx3^r#-`~<{ z;9X<9$)qW?ytUK7UG&-AYcqe}^a#`aw1y>7=}e>EaqG_~UzF`t_i2f=Jk$L3=hBJq zs{CefY)+bCoqqe~is`ljYKPW{JX@Kv8fdpz&eL1`Tl$d84Ci*s zWqOM0F#Z^|b{ZTmnHddAAq(_Ap901B)l|Qkhg@bH-1R0NB)miK z_cTqP7Re2#>r%Od6WiWw@&`#f?tT*slD?r=4VK<lD%!z5+KdA_B_%GkTehMe3@YYm^mi4H$2CN2vi?(#~eZJ*o!C8<`)MX8A;sSHL2hL*a9M!H7EAqJLK#wJ#VX4(b@Rt5$Qm;dfU(U6;; zl9^VCTf@gbS5R0=f@}!RPb(=;EJ|f?Ovz75Rq)JBOiv9;O-!jQJeg_(3UE(XKbLh* G2~7YIWw#>$ literal 0 HcmV?d00001 From d1173e0e735ea2dc857653cee2c37c2a9d5a03cb Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Tue, 28 Feb 2023 09:50:49 +0200 Subject: [PATCH 017/891] fix: js references --- static/js/index.js | 2 +- templates/nostrmarket/index.html | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/static/js/index.js b/static/js/index.js index b48f3b1..71d65d5 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -1,7 +1,7 @@ const stalls = async () => { Vue.component(VueQrcode.name, VueQrcode) - await relayDetails('static/components/stall-details/stall-details.html') + await stallDetails('static/components/stall-details/stall-details.html') new Vue({ el: '#vue', diff --git a/templates/nostrmarket/index.html b/templates/nostrmarket/index.html index 86f0077..cf4ff44 100644 --- a/templates/nostrmarket/index.html +++ b/templates/nostrmarket/index.html @@ -22,8 +22,8 @@ {% endblock%}{% block scripts %} {{ window_vars(user) }} - - - + + + {% endblock %} From 6f3154a449c0f109cecb04b8e00a50b0b08f1720 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Tue, 28 Feb 2023 09:50:49 +0200 Subject: [PATCH 018/891] fix: js references --- static/js/index.js | 2 +- templates/nostrmarket/index.html | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/static/js/index.js b/static/js/index.js index b48f3b1..71d65d5 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -1,7 +1,7 @@ const stalls = async () => { Vue.component(VueQrcode.name, VueQrcode) - await relayDetails('static/components/stall-details/stall-details.html') + await stallDetails('static/components/stall-details/stall-details.html') new Vue({ el: '#vue', diff --git a/templates/nostrmarket/index.html b/templates/nostrmarket/index.html index 86f0077..cf4ff44 100644 --- a/templates/nostrmarket/index.html +++ b/templates/nostrmarket/index.html @@ -22,8 +22,8 @@ {% endblock%}{% block scripts %} {{ window_vars(user) }} - - - + + + {% endblock %} From 1a458ee757038be53aa2da1e573a64a55e8328c4 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Tue, 28 Feb 2023 10:24:27 +0200 Subject: [PATCH 019/891] refactor: clean migration statements --- migrations.py | 152 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 152 insertions(+) diff --git a/migrations.py b/migrations.py index e69de29..02565f7 100644 --- a/migrations.py +++ b/migrations.py @@ -0,0 +1,152 @@ +async def m001_initial(db): + + """ + Initial merchants table. + """ + await db.execute( + """ + CREATE TABLE nostrmarket.merchants ( + user_id TEXT NOT NULL, + private_key TEXT NOT NULL, + public_key TEXT NOT NULL, + config TEXT NOT NULL + ); + """ + ) + + """ + Initial stalls table. + """ + await db.execute( + """ + CREATE TABLE nostrmarket.stalls ( + id TEXT PRIMARY KEY, + wallet TEXT NOT NULL, + name TEXT NOT NULL, + currency TEXT, + shipping_zones TEXT NOT NULL, + rating REAL DEFAULT 0 + ); + """ + ) + + """ + Initial products table. + """ + await db.execute( + f""" + CREATE TABLE nostrmarket.products ( + id TEXT PRIMARY KEY, + stall_id TEXT NOT NULL, + product TEXT NOT NULL, + categories TEXT, + description TEXT, + image TEXT, + price REAL NOT NULL, + quantity INTEGER NOT NULL, + rating REAL DEFAULT 0 + ); + """ + ) + + """ + Initial zones table. + """ + await db.execute( + """ + CREATE TABLE nostrmarket.zones ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL, + cost REAL NOT NULL, + countries TEXT NOT NULL + ); + """ + ) + + """ + Initial orders table. + """ + await db.execute( + f""" + CREATE TABLE nostrmarket.orders ( + id TEXT PRIMARY KEY, + wallet TEXT NOT NULL, + username TEXT, + pubkey TEXT, + shipping_zone TEXT NOT NULL, + address TEXT, + email TEXT, + total REAL NOT NULL, + invoice_id 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 nostrmarket.order_details ( + id TEXT PRIMARY KEY, + order_id TEXT NOT NULL, + product_id TEXT NOT NULL, + quantity INTEGER NOT NULL + ); + """ + ) + + """ + Initial market table. + """ + await db.execute( + """ + CREATE TABLE nostrmarket.markets ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL, + name TEXT + ); + """ + ) + + """ + Initial market stalls table. + """ + await db.execute( + f""" + CREATE TABLE nostrmarket.market_stalls ( + id TEXT PRIMARY KEY, + market_id TEXT NOT NULL, + stall_id TEXT NOT NULL + ); + """ + ) + + """ + Initial chat messages table. + """ + await db.execute( + f""" + CREATE TABLE nostrmarket.messages ( + id TEXT PRIMARY KEY, + msg TEXT NOT NULL, + pubkey TEXT NOT NULL, + conversation_id TEXT NOT NULL, + timestamp TIMESTAMP NOT NULL DEFAULT {db.timestamp_now} + ); + """ + ) + + if db.type != "SQLITE": + """ + Create indexes for message fetching + """ + await db.execute( + "CREATE INDEX idx_messages_timestamp ON nostrmarket.messages (timestamp DESC)" + ) + await db.execute( + "CREATE INDEX idx_messages_conversations ON nostrmarket.messages (conversation_id)" + ) From e3884b13cee7a3488c88504be0fe2e326ff81db9 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Tue, 28 Feb 2023 10:24:27 +0200 Subject: [PATCH 020/891] refactor: clean migration statements --- migrations.py | 152 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 152 insertions(+) diff --git a/migrations.py b/migrations.py index e69de29..02565f7 100644 --- a/migrations.py +++ b/migrations.py @@ -0,0 +1,152 @@ +async def m001_initial(db): + + """ + Initial merchants table. + """ + await db.execute( + """ + CREATE TABLE nostrmarket.merchants ( + user_id TEXT NOT NULL, + private_key TEXT NOT NULL, + public_key TEXT NOT NULL, + config TEXT NOT NULL + ); + """ + ) + + """ + Initial stalls table. + """ + await db.execute( + """ + CREATE TABLE nostrmarket.stalls ( + id TEXT PRIMARY KEY, + wallet TEXT NOT NULL, + name TEXT NOT NULL, + currency TEXT, + shipping_zones TEXT NOT NULL, + rating REAL DEFAULT 0 + ); + """ + ) + + """ + Initial products table. + """ + await db.execute( + f""" + CREATE TABLE nostrmarket.products ( + id TEXT PRIMARY KEY, + stall_id TEXT NOT NULL, + product TEXT NOT NULL, + categories TEXT, + description TEXT, + image TEXT, + price REAL NOT NULL, + quantity INTEGER NOT NULL, + rating REAL DEFAULT 0 + ); + """ + ) + + """ + Initial zones table. + """ + await db.execute( + """ + CREATE TABLE nostrmarket.zones ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL, + cost REAL NOT NULL, + countries TEXT NOT NULL + ); + """ + ) + + """ + Initial orders table. + """ + await db.execute( + f""" + CREATE TABLE nostrmarket.orders ( + id TEXT PRIMARY KEY, + wallet TEXT NOT NULL, + username TEXT, + pubkey TEXT, + shipping_zone TEXT NOT NULL, + address TEXT, + email TEXT, + total REAL NOT NULL, + invoice_id 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 nostrmarket.order_details ( + id TEXT PRIMARY KEY, + order_id TEXT NOT NULL, + product_id TEXT NOT NULL, + quantity INTEGER NOT NULL + ); + """ + ) + + """ + Initial market table. + """ + await db.execute( + """ + CREATE TABLE nostrmarket.markets ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL, + name TEXT + ); + """ + ) + + """ + Initial market stalls table. + """ + await db.execute( + f""" + CREATE TABLE nostrmarket.market_stalls ( + id TEXT PRIMARY KEY, + market_id TEXT NOT NULL, + stall_id TEXT NOT NULL + ); + """ + ) + + """ + Initial chat messages table. + """ + await db.execute( + f""" + CREATE TABLE nostrmarket.messages ( + id TEXT PRIMARY KEY, + msg TEXT NOT NULL, + pubkey TEXT NOT NULL, + conversation_id TEXT NOT NULL, + timestamp TIMESTAMP NOT NULL DEFAULT {db.timestamp_now} + ); + """ + ) + + if db.type != "SQLITE": + """ + Create indexes for message fetching + """ + await db.execute( + "CREATE INDEX idx_messages_timestamp ON nostrmarket.messages (timestamp DESC)" + ) + await db.execute( + "CREATE INDEX idx_messages_conversations ON nostrmarket.messages (conversation_id)" + ) From b1d0c5c4754d76a951c48b805e5fd15ffcba7abf Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Tue, 28 Feb 2023 10:39:11 +0200 Subject: [PATCH 021/891] feat: add landing page --- tasks.py | 2 +- templates/nostrmarket/index.html | 55 +++++++++++++++++++++++++++++++- 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/tasks.py b/tasks.py index cbf1b74..3254dcc 100644 --- a/tasks.py +++ b/tasks.py @@ -21,7 +21,7 @@ async def wait_for_paid_invoices(): async def on_invoice_paid(payment: Payment) -> None: - if payment.extra.get("tag") != "market": + if payment.extra.get("tag") != "nostrmarket": return print("### on_invoice_paid") diff --git a/templates/nostrmarket/index.html b/templates/nostrmarket/index.html index cf4ff44..410fbf4 100644 --- a/templates/nostrmarket/index.html +++ b/templates/nostrmarket/index.html @@ -3,7 +3,60 @@
- section + + Wellcome to Nostr Market!
+ In Nostr Market, merchant and customer communicate via NOSTR relays, so + loss of money, product information, and reputation become far less + likely if attacked. +
+ + Terms
+
    +
  • + merchant - seller of products with + NOSTR key-pair +
  • +
  • + customer - buyer of products with + NOSTR key-pair +
  • +
  • + product - item for sale by the + merchant +
  • +
  • + stall - list of products controlled + by merchant (a merchant can have multiple stalls) +
  • +
  • + marketplace - clientside software for + searching stalls and purchasing products +
  • +
+
+ +
+
+ + Use an existing private key (hex or npub) + + + A new key pair will be generated for you + +
+
+
From 375b21462514b32814be1ba7e19e72395f551c8b Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Tue, 28 Feb 2023 10:39:11 +0200 Subject: [PATCH 022/891] feat: add landing page --- tasks.py | 2 +- templates/nostrmarket/index.html | 55 +++++++++++++++++++++++++++++++- 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/tasks.py b/tasks.py index cbf1b74..3254dcc 100644 --- a/tasks.py +++ b/tasks.py @@ -21,7 +21,7 @@ async def wait_for_paid_invoices(): async def on_invoice_paid(payment: Payment) -> None: - if payment.extra.get("tag") != "market": + if payment.extra.get("tag") != "nostrmarket": return print("### on_invoice_paid") diff --git a/templates/nostrmarket/index.html b/templates/nostrmarket/index.html index cf4ff44..410fbf4 100644 --- a/templates/nostrmarket/index.html +++ b/templates/nostrmarket/index.html @@ -3,7 +3,60 @@
- section + + Wellcome to Nostr Market!
+ In Nostr Market, merchant and customer communicate via NOSTR relays, so + loss of money, product information, and reputation become far less + likely if attacked. +
+ + Terms
+
    +
  • + merchant - seller of products with + NOSTR key-pair +
  • +
  • + customer - buyer of products with + NOSTR key-pair +
  • +
  • + product - item for sale by the + merchant +
  • +
  • + stall - list of products controlled + by merchant (a merchant can have multiple stalls) +
  • +
  • + marketplace - clientside software for + searching stalls and purchasing products +
  • +
+
+ +
+
+ + Use an existing private key (hex or npub) + + + A new key pair will be generated for you + +
+
+
From 2832ee928c8f0f7d525f9a4f1eed9884807df6d6 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Tue, 28 Feb 2023 10:41:35 +0200 Subject: [PATCH 023/891] doc: update created by --- templates/nostrmarket/_api_docs.html | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/templates/nostrmarket/_api_docs.html b/templates/nostrmarket/_api_docs.html index 9ed2f47..07e30eb 100644 --- a/templates/nostrmarket/_api_docs.html +++ b/templates/nostrmarket/_api_docs.html @@ -4,6 +4,20 @@ Nostr Market
Created by, + Tal Vasconcelos + Ben Arc Date: Tue, 28 Feb 2023 10:41:35 +0200 Subject: [PATCH 024/891] doc: update created by --- templates/nostrmarket/_api_docs.html | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/templates/nostrmarket/_api_docs.html b/templates/nostrmarket/_api_docs.html index 9ed2f47..07e30eb 100644 --- a/templates/nostrmarket/_api_docs.html +++ b/templates/nostrmarket/_api_docs.html @@ -4,6 +4,20 @@ Nostr Market
Created by, +
Tal Vasconcelos + Ben Arc Date: Tue, 28 Feb 2023 11:46:40 +0200 Subject: [PATCH 025/891] feat: init merchant --- crud.py | 42 +++++++++++++++++++++++++++++ migrations.py | 3 ++- models.py | 25 ++++++++++++++++++ static/js/index.js | 43 +++++++++++++++++++++++++++--- templates/nostrmarket/index.html | 14 +++++++--- views_api.py | 45 ++++++++++++++++++++++++++++++++ 6 files changed, 164 insertions(+), 8 deletions(-) create mode 100644 crud.py create mode 100644 models.py diff --git a/crud.py b/crud.py new file mode 100644 index 0000000..8f41bff --- /dev/null +++ b/crud.py @@ -0,0 +1,42 @@ +import json +from typing import Optional + +from lnbits.helpers import urlsafe_short_hash + +from . import db +from .models import Merchant, PartialMerchant + + +async def create_merchant(user_id: str, m: PartialMerchant) -> Merchant: + merchant_id = urlsafe_short_hash() + await db.execute( + """ + INSERT INTO nostrmarket.merchants (user_id, id, private_key, public_key, meta) + VALUES (?, ?, ?, ?, ?) + """, + (user_id, merchant_id, m.private_key, m.public_key, json.dumps(dict(m.config))), + ) + merchant = await get_merchant(user_id, merchant_id) + assert merchant, "Created merchant cannot be retrieved" + return merchant + + +async def get_merchant(user_id: str, merchant_id: str) -> Optional[Merchant]: + row = await db.fetchone( + """SELECT * FROM nostrmarket.merchants WHERE user_id = ? AND id = ?""", + ( + user_id, + merchant_id, + ), + ) + + return Merchant.from_row(row) if row else None + + +async def get_merchant_for_user(user_id: str) -> Optional[Merchant]: + row = await db.fetchone( + """SELECT * FROM nostrmarket.merchants WHERE user_id = ? """, + (user_id,), + ) + + return Merchant.from_row(row) if row else None diff --git a/migrations.py b/migrations.py index 02565f7..0880b62 100644 --- a/migrations.py +++ b/migrations.py @@ -7,9 +7,10 @@ async def m001_initial(db): """ CREATE TABLE nostrmarket.merchants ( user_id TEXT NOT NULL, + id TEXT PRIMARY KEY, private_key TEXT NOT NULL, public_key TEXT NOT NULL, - config TEXT NOT NULL + meta TEXT NOT NULL DEFAULT '{}' ); """ ) diff --git a/models.py b/models.py new file mode 100644 index 0000000..e6a4e5e --- /dev/null +++ b/models.py @@ -0,0 +1,25 @@ +import json +from sqlite3 import Row +from typing import Optional + +from pydantic import BaseModel + + +class MerchantConfig(BaseModel): + name: Optional[str] + + +class PartialMerchant(BaseModel): + private_key: str + public_key: str + config: MerchantConfig = MerchantConfig() + + +class Merchant(PartialMerchant): + id: str + + @classmethod + def from_row(cls, row: Row) -> "Merchant": + merchant = cls(**dict(row)) + merchant.config = MerchantConfig(**json.loads(row["meta"])) + return merchant diff --git a/static/js/index.js b/static/js/index.js index 71d65d5..a5adbd6 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -1,15 +1,52 @@ -const stalls = async () => { +const merchant = async () => { Vue.component(VueQrcode.name, VueQrcode) await stallDetails('static/components/stall-details/stall-details.html') + const nostr = window.NostrTools + new Vue({ el: '#vue', mixins: [windowMixin], data: function () { - return {} + return { + merchant: null + } + }, + methods: { + generateKeys: async function () { + const privkey = nostr.generatePrivateKey() + const pubkey = nostr.getPublicKey(privkey) + + const data = {private_key: privkey, public_key: pubkey, config: {}} + try { + const resp = await LNbits.api.request( + 'POST', + '/nostrmarket/api/v1/merchant', + this.g.user.wallets[0].adminkey, + data + ) + } catch (error) { + LNbits.utils.notifyApiError(error) + } + }, + getMerchant: async function () { + try { + const {data} = await LNbits.api.request( + 'get', + '/nostrmarket/api/v1/merchant', + this.g.user.wallets[0].adminkey + ) + this.merchant = data + } catch (error) { + LNbits.utils.notifyApiError(error) + } + } + }, + created: async function () { + await this.getMerchant() } }) } -stalls() +merchant() diff --git a/templates/nostrmarket/index.html b/templates/nostrmarket/index.html index 410fbf4..37710a5 100644 --- a/templates/nostrmarket/index.html +++ b/templates/nostrmarket/index.html @@ -2,7 +2,7 @@ %} {% block page %}
- + Wellcome to Nostr Market!
In Nostr Market, merchant and customer communicate via NOSTR relays, so @@ -41,15 +41,14 @@ disabled label="Import Key" color="primary" - @click="importKey" class="float-left" > Use an existing private key (hex or npub) A new key pair will be generated for you @@ -58,6 +57,11 @@
+
+ + Merchant Exists + +
@@ -75,6 +79,8 @@
{% endblock%}{% block scripts %} {{ window_vars(user) }} + + diff --git a/views_api.py b/views_api.py index e69de29..ad4d66e 100644 --- a/views_api.py +++ b/views_api.py @@ -0,0 +1,45 @@ +from http import HTTPStatus +from typing import Optional + +from fastapi import Depends +from fastapi.exceptions import HTTPException +from loguru import logger + +from lnbits.decorators import WalletTypeInfo, require_admin_key, require_invoice_key + +from . import nostrmarket_ext +from .crud import create_merchant, get_merchant_for_user +from .models import Merchant, PartialMerchant + + +@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 create merchant", + ) From b0ffbe0b4c616d59da3defbcef6befd0809b00bc Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Tue, 28 Feb 2023 11:46:40 +0200 Subject: [PATCH 026/891] feat: init merchant --- crud.py | 42 +++++++++++++++++++++++++++++ migrations.py | 3 ++- models.py | 25 ++++++++++++++++++ static/js/index.js | 43 +++++++++++++++++++++++++++--- templates/nostrmarket/index.html | 14 +++++++--- views_api.py | 45 ++++++++++++++++++++++++++++++++ 6 files changed, 164 insertions(+), 8 deletions(-) create mode 100644 crud.py create mode 100644 models.py diff --git a/crud.py b/crud.py new file mode 100644 index 0000000..8f41bff --- /dev/null +++ b/crud.py @@ -0,0 +1,42 @@ +import json +from typing import Optional + +from lnbits.helpers import urlsafe_short_hash + +from . import db +from .models import Merchant, PartialMerchant + + +async def create_merchant(user_id: str, m: PartialMerchant) -> Merchant: + merchant_id = urlsafe_short_hash() + await db.execute( + """ + INSERT INTO nostrmarket.merchants (user_id, id, private_key, public_key, meta) + VALUES (?, ?, ?, ?, ?) + """, + (user_id, merchant_id, m.private_key, m.public_key, json.dumps(dict(m.config))), + ) + merchant = await get_merchant(user_id, merchant_id) + assert merchant, "Created merchant cannot be retrieved" + return merchant + + +async def get_merchant(user_id: str, merchant_id: str) -> Optional[Merchant]: + row = await db.fetchone( + """SELECT * FROM nostrmarket.merchants WHERE user_id = ? AND id = ?""", + ( + user_id, + merchant_id, + ), + ) + + return Merchant.from_row(row) if row else None + + +async def get_merchant_for_user(user_id: str) -> Optional[Merchant]: + row = await db.fetchone( + """SELECT * FROM nostrmarket.merchants WHERE user_id = ? """, + (user_id,), + ) + + return Merchant.from_row(row) if row else None diff --git a/migrations.py b/migrations.py index 02565f7..0880b62 100644 --- a/migrations.py +++ b/migrations.py @@ -7,9 +7,10 @@ async def m001_initial(db): """ CREATE TABLE nostrmarket.merchants ( user_id TEXT NOT NULL, + id TEXT PRIMARY KEY, private_key TEXT NOT NULL, public_key TEXT NOT NULL, - config TEXT NOT NULL + meta TEXT NOT NULL DEFAULT '{}' ); """ ) diff --git a/models.py b/models.py new file mode 100644 index 0000000..e6a4e5e --- /dev/null +++ b/models.py @@ -0,0 +1,25 @@ +import json +from sqlite3 import Row +from typing import Optional + +from pydantic import BaseModel + + +class MerchantConfig(BaseModel): + name: Optional[str] + + +class PartialMerchant(BaseModel): + private_key: str + public_key: str + config: MerchantConfig = MerchantConfig() + + +class Merchant(PartialMerchant): + id: str + + @classmethod + def from_row(cls, row: Row) -> "Merchant": + merchant = cls(**dict(row)) + merchant.config = MerchantConfig(**json.loads(row["meta"])) + return merchant diff --git a/static/js/index.js b/static/js/index.js index 71d65d5..a5adbd6 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -1,15 +1,52 @@ -const stalls = async () => { +const merchant = async () => { Vue.component(VueQrcode.name, VueQrcode) await stallDetails('static/components/stall-details/stall-details.html') + const nostr = window.NostrTools + new Vue({ el: '#vue', mixins: [windowMixin], data: function () { - return {} + return { + merchant: null + } + }, + methods: { + generateKeys: async function () { + const privkey = nostr.generatePrivateKey() + const pubkey = nostr.getPublicKey(privkey) + + const data = {private_key: privkey, public_key: pubkey, config: {}} + try { + const resp = await LNbits.api.request( + 'POST', + '/nostrmarket/api/v1/merchant', + this.g.user.wallets[0].adminkey, + data + ) + } catch (error) { + LNbits.utils.notifyApiError(error) + } + }, + getMerchant: async function () { + try { + const {data} = await LNbits.api.request( + 'get', + '/nostrmarket/api/v1/merchant', + this.g.user.wallets[0].adminkey + ) + this.merchant = data + } catch (error) { + LNbits.utils.notifyApiError(error) + } + } + }, + created: async function () { + await this.getMerchant() } }) } -stalls() +merchant() diff --git a/templates/nostrmarket/index.html b/templates/nostrmarket/index.html index 410fbf4..37710a5 100644 --- a/templates/nostrmarket/index.html +++ b/templates/nostrmarket/index.html @@ -2,7 +2,7 @@ %} {% block page %}
- + Wellcome to Nostr Market!
In Nostr Market, merchant and customer communicate via NOSTR relays, so @@ -41,15 +41,14 @@ disabled label="Import Key" color="primary" - @click="importKey" class="float-left" > Use an existing private key (hex or npub) A new key pair will be generated for you @@ -58,6 +57,11 @@
+
+ + Merchant Exists + +
@@ -75,6 +79,8 @@
{% endblock%}{% block scripts %} {{ window_vars(user) }} + + diff --git a/views_api.py b/views_api.py index e69de29..ad4d66e 100644 --- a/views_api.py +++ b/views_api.py @@ -0,0 +1,45 @@ +from http import HTTPStatus +from typing import Optional + +from fastapi import Depends +from fastapi.exceptions import HTTPException +from loguru import logger + +from lnbits.decorators import WalletTypeInfo, require_admin_key, require_invoice_key + +from . import nostrmarket_ext +from .crud import create_merchant, get_merchant_for_user +from .models import Merchant, PartialMerchant + + +@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 create merchant", + ) From f568b1b927bfc76437ed8ffb8b248bb26d563891 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Tue, 28 Feb 2023 12:21:34 +0200 Subject: [PATCH 027/891] feat: show merchant keypair --- static/components/key-pair/key-pair.html | 44 ++++++++++++++++++++++++ static/components/key-pair/key-pair.js | 25 ++++++++++++++ static/js/index.js | 6 ++-- templates/nostrmarket/index.html | 22 +++++++++++- 4 files changed, 94 insertions(+), 3 deletions(-) create mode 100644 static/components/key-pair/key-pair.html create mode 100644 static/components/key-pair/key-pair.js diff --git a/static/components/key-pair/key-pair.html b/static/components/key-pair/key-pair.html new file mode 100644 index 0000000..a0657fa --- /dev/null +++ b/static/components/key-pair/key-pair.html @@ -0,0 +1,44 @@ +
+ +
+
Public Key
+
+ + Show Private Key + +
+
+ +
+
+
+ + + +
Click to copy
+
+
+
+
+
+ + + +
Click to copy
+
+
+
+
+
diff --git a/static/components/key-pair/key-pair.js b/static/components/key-pair/key-pair.js new file mode 100644 index 0000000..bee16b4 --- /dev/null +++ b/static/components/key-pair/key-pair.js @@ -0,0 +1,25 @@ +async function keyPair(path) { + const template = await loadTemplateAsync(path) + Vue.component('key-pair', { + name: 'key-pair', + template, + + props: ['public-key', 'private-key'], + data: function () { + return { + showPrivateKey: false + } + }, + methods: { + copyText: function (text, message, position) { + var notify = this.$q.notify + Quasar.utils.copyToClipboard(text).then(function () { + notify({ + message: message || 'Copied to clipboard!', + position: position || 'bottom' + }) + }) + } + } + }) +} diff --git a/static/js/index.js b/static/js/index.js index a5adbd6..5bc293f 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -2,6 +2,7 @@ const merchant = async () => { Vue.component(VueQrcode.name, VueQrcode) await stallDetails('static/components/stall-details/stall-details.html') + await keyPair('static/components/key-pair/key-pair.html') const nostr = window.NostrTools @@ -10,7 +11,8 @@ const merchant = async () => { mixins: [windowMixin], data: function () { return { - merchant: null + merchant: {}, + showKeys: false } }, methods: { @@ -20,7 +22,7 @@ const merchant = async () => { const data = {private_key: privkey, public_key: pubkey, config: {}} try { - const resp = await LNbits.api.request( + await LNbits.api.request( 'POST', '/nostrmarket/api/v1/merchant', this.g.user.wallets[0].adminkey, diff --git a/templates/nostrmarket/index.html b/templates/nostrmarket/index.html index 37710a5..fc154b4 100644 --- a/templates/nostrmarket/index.html +++ b/templates/nostrmarket/index.html @@ -59,7 +59,26 @@
- Merchant Exists + +
+
+ + Show Public or Private keys + +
+
+
+ + +
@@ -83,6 +102,7 @@ + {% endblock %} From e1745703f60cafd761fc6cedd569e47fc56d7c58 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Tue, 28 Feb 2023 12:21:34 +0200 Subject: [PATCH 028/891] feat: show merchant keypair --- static/components/key-pair/key-pair.html | 44 ++++++++++++++++++++++++ static/components/key-pair/key-pair.js | 25 ++++++++++++++ static/js/index.js | 6 ++-- templates/nostrmarket/index.html | 22 +++++++++++- 4 files changed, 94 insertions(+), 3 deletions(-) create mode 100644 static/components/key-pair/key-pair.html create mode 100644 static/components/key-pair/key-pair.js diff --git a/static/components/key-pair/key-pair.html b/static/components/key-pair/key-pair.html new file mode 100644 index 0000000..a0657fa --- /dev/null +++ b/static/components/key-pair/key-pair.html @@ -0,0 +1,44 @@ +
+ +
+
Public Key
+
+ + Show Private Key + +
+
+ +
+
+
+ + + +
Click to copy
+
+
+
+
+
+ + + +
Click to copy
+
+
+
+
+
diff --git a/static/components/key-pair/key-pair.js b/static/components/key-pair/key-pair.js new file mode 100644 index 0000000..bee16b4 --- /dev/null +++ b/static/components/key-pair/key-pair.js @@ -0,0 +1,25 @@ +async function keyPair(path) { + const template = await loadTemplateAsync(path) + Vue.component('key-pair', { + name: 'key-pair', + template, + + props: ['public-key', 'private-key'], + data: function () { + return { + showPrivateKey: false + } + }, + methods: { + copyText: function (text, message, position) { + var notify = this.$q.notify + Quasar.utils.copyToClipboard(text).then(function () { + notify({ + message: message || 'Copied to clipboard!', + position: position || 'bottom' + }) + }) + } + } + }) +} diff --git a/static/js/index.js b/static/js/index.js index a5adbd6..5bc293f 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -2,6 +2,7 @@ const merchant = async () => { Vue.component(VueQrcode.name, VueQrcode) await stallDetails('static/components/stall-details/stall-details.html') + await keyPair('static/components/key-pair/key-pair.html') const nostr = window.NostrTools @@ -10,7 +11,8 @@ const merchant = async () => { mixins: [windowMixin], data: function () { return { - merchant: null + merchant: {}, + showKeys: false } }, methods: { @@ -20,7 +22,7 @@ const merchant = async () => { const data = {private_key: privkey, public_key: pubkey, config: {}} try { - const resp = await LNbits.api.request( + await LNbits.api.request( 'POST', '/nostrmarket/api/v1/merchant', this.g.user.wallets[0].adminkey, diff --git a/templates/nostrmarket/index.html b/templates/nostrmarket/index.html index 37710a5..fc154b4 100644 --- a/templates/nostrmarket/index.html +++ b/templates/nostrmarket/index.html @@ -59,7 +59,26 @@
- Merchant Exists + +
+
+ + Show Public or Private keys + +
+
+
+ + +
@@ -83,6 +102,7 @@ + {% endblock %} From 2b106a203e0d3c4f3112322b4cd9d04d5e95738d Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Tue, 28 Feb 2023 12:28:40 +0200 Subject: [PATCH 029/891] refactor: rename product column --- migrations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/migrations.py b/migrations.py index 0880b62..51567b5 100644 --- a/migrations.py +++ b/migrations.py @@ -39,7 +39,7 @@ async def m001_initial(db): CREATE TABLE nostrmarket.products ( id TEXT PRIMARY KEY, stall_id TEXT NOT NULL, - product TEXT NOT NULL, + name TEXT NOT NULL, categories TEXT, description TEXT, image TEXT, From 8d957002eb26cd9e02ecedcb657db14fe8a546e9 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Tue, 28 Feb 2023 12:28:40 +0200 Subject: [PATCH 030/891] refactor: rename product column --- migrations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/migrations.py b/migrations.py index 0880b62..51567b5 100644 --- a/migrations.py +++ b/migrations.py @@ -39,7 +39,7 @@ async def m001_initial(db): CREATE TABLE nostrmarket.products ( id TEXT PRIMARY KEY, stall_id TEXT NOT NULL, - product TEXT NOT NULL, + name TEXT NOT NULL, categories TEXT, description TEXT, image TEXT, From 7c15f8ff45631ef4edaad2ebe40a552503230a37 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Tue, 28 Feb 2023 12:28:51 +0200 Subject: [PATCH 031/891] fix: set merchant after create --- static/js/index.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/static/js/index.js b/static/js/index.js index 5bc293f..561c6ec 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -20,14 +20,19 @@ const merchant = async () => { const privkey = nostr.generatePrivateKey() const pubkey = nostr.getPublicKey(privkey) - const data = {private_key: privkey, public_key: pubkey, config: {}} + const payload = {private_key: privkey, public_key: pubkey, config: {}} try { - await LNbits.api.request( + const {data} = await LNbits.api.request( 'POST', '/nostrmarket/api/v1/merchant', this.g.user.wallets[0].adminkey, - data + payload ) + this.merchant = data + this.$q.notify({ + type: 'positive', + message: 'Keys generated!' + }) } catch (error) { LNbits.utils.notifyApiError(error) } From 03a1ccb0b238bb052109209121a01bdfc3d1d55b Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Tue, 28 Feb 2023 12:28:51 +0200 Subject: [PATCH 032/891] fix: set merchant after create --- static/js/index.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/static/js/index.js b/static/js/index.js index 5bc293f..561c6ec 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -20,14 +20,19 @@ const merchant = async () => { const privkey = nostr.generatePrivateKey() const pubkey = nostr.getPublicKey(privkey) - const data = {private_key: privkey, public_key: pubkey, config: {}} + const payload = {private_key: privkey, public_key: pubkey, config: {}} try { - await LNbits.api.request( + const {data} = await LNbits.api.request( 'POST', '/nostrmarket/api/v1/merchant', this.g.user.wallets[0].adminkey, - data + payload ) + this.merchant = data + this.$q.notify({ + type: 'positive', + message: 'Keys generated!' + }) } catch (error) { LNbits.utils.notifyApiError(error) } From dcda99830e4f7fd027ba9a91c3374e0c5938eaa9 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Tue, 28 Feb 2023 13:08:58 +0200 Subject: [PATCH 033/891] feat: basic shipping zones UI --- migrations.py | 1 + .../shipping-zones/shipping-zones.html | 31 +++++++++++++++++++ .../shipping-zones/shipping-zones.js | 19 ++++++++++++ static/js/index.js | 2 ++ templates/nostrmarket/index.html | 8 +++-- 5 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 static/components/shipping-zones/shipping-zones.html create mode 100644 static/components/shipping-zones/shipping-zones.js diff --git a/migrations.py b/migrations.py index 51567b5..b3fafbe 100644 --- a/migrations.py +++ b/migrations.py @@ -58,6 +58,7 @@ async def m001_initial(db): CREATE TABLE nostrmarket.zones ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL, + name TEXT NOT NULL, cost REAL NOT NULL, countries TEXT NOT NULL ); diff --git a/static/components/shipping-zones/shipping-zones.html b/static/components/shipping-zones/shipping-zones.html new file mode 100644 index 0000000..08ced67 --- /dev/null +++ b/static/components/shipping-zones/shipping-zones.html @@ -0,0 +1,31 @@ +
+ + + + + New Shipping Zone + Create a new shipping zone + + + + + XXX + xxxxxxxxxxxxx + + + +
diff --git a/static/components/shipping-zones/shipping-zones.js b/static/components/shipping-zones/shipping-zones.js new file mode 100644 index 0000000..4aef5f8 --- /dev/null +++ b/static/components/shipping-zones/shipping-zones.js @@ -0,0 +1,19 @@ +async function shippingZones(path) { + const template = await loadTemplateAsync(path) + Vue.component('shipping-zones', { + name: 'shipping-zones', + template, + + data: function () { + return { + zones: [] + } + }, + methods: { + createShippingZone: async function () { + console.log('### createShippingZone', createShippingZone) + }, + editShippingZone: async function () {} + } + }) +} diff --git a/static/js/index.js b/static/js/index.js index 561c6ec..6d55b30 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -3,6 +3,7 @@ const merchant = async () => { await stallDetails('static/components/stall-details/stall-details.html') await keyPair('static/components/key-pair/key-pair.html') + await shippingZones('static/components/shipping-zones/shipping-zones.html') const nostr = window.NostrTools @@ -12,6 +13,7 @@ const merchant = async () => { data: function () { return { merchant: {}, + shippingZones: [], showKeys: false } }, diff --git a/templates/nostrmarket/index.html b/templates/nostrmarket/index.html index fc154b4..f7aaa6c 100644 --- a/templates/nostrmarket/index.html +++ b/templates/nostrmarket/index.html @@ -61,12 +61,15 @@
-
+
+
+ +
+
Show Public or Private keys @@ -103,6 +106,7 @@ + {% endblock %} From dc6ae1d9b339d26f62e6cd1f77532b6161e8ff16 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Tue, 28 Feb 2023 13:08:58 +0200 Subject: [PATCH 034/891] feat: basic shipping zones UI --- migrations.py | 1 + .../shipping-zones/shipping-zones.html | 31 +++++++++++++++++++ .../shipping-zones/shipping-zones.js | 19 ++++++++++++ static/js/index.js | 2 ++ templates/nostrmarket/index.html | 8 +++-- 5 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 static/components/shipping-zones/shipping-zones.html create mode 100644 static/components/shipping-zones/shipping-zones.js diff --git a/migrations.py b/migrations.py index 51567b5..b3fafbe 100644 --- a/migrations.py +++ b/migrations.py @@ -58,6 +58,7 @@ async def m001_initial(db): CREATE TABLE nostrmarket.zones ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL, + name TEXT NOT NULL, cost REAL NOT NULL, countries TEXT NOT NULL ); diff --git a/static/components/shipping-zones/shipping-zones.html b/static/components/shipping-zones/shipping-zones.html new file mode 100644 index 0000000..08ced67 --- /dev/null +++ b/static/components/shipping-zones/shipping-zones.html @@ -0,0 +1,31 @@ +
+ + + + + New Shipping Zone + Create a new shipping zone + + + + + XXX + xxxxxxxxxxxxx + + + +
diff --git a/static/components/shipping-zones/shipping-zones.js b/static/components/shipping-zones/shipping-zones.js new file mode 100644 index 0000000..4aef5f8 --- /dev/null +++ b/static/components/shipping-zones/shipping-zones.js @@ -0,0 +1,19 @@ +async function shippingZones(path) { + const template = await loadTemplateAsync(path) + Vue.component('shipping-zones', { + name: 'shipping-zones', + template, + + data: function () { + return { + zones: [] + } + }, + methods: { + createShippingZone: async function () { + console.log('### createShippingZone', createShippingZone) + }, + editShippingZone: async function () {} + } + }) +} diff --git a/static/js/index.js b/static/js/index.js index 561c6ec..6d55b30 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -3,6 +3,7 @@ const merchant = async () => { await stallDetails('static/components/stall-details/stall-details.html') await keyPair('static/components/key-pair/key-pair.html') + await shippingZones('static/components/shipping-zones/shipping-zones.html') const nostr = window.NostrTools @@ -12,6 +13,7 @@ const merchant = async () => { data: function () { return { merchant: {}, + shippingZones: [], showKeys: false } }, diff --git a/templates/nostrmarket/index.html b/templates/nostrmarket/index.html index fc154b4..f7aaa6c 100644 --- a/templates/nostrmarket/index.html +++ b/templates/nostrmarket/index.html @@ -61,12 +61,15 @@
-
+
+
+ +
+
Show Public or Private keys @@ -103,6 +106,7 @@ + {% endblock %} From 31c5a82cb98a4de954ae027f09fdf230c4cfccb3 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Tue, 28 Feb 2023 16:30:09 +0200 Subject: [PATCH 035/891] feat: manage shipping zones --- crud.py | 69 ++++++- migrations.py | 3 +- models.py | 22 ++- .../shipping-zones/shipping-zones.html | 77 +++++++- .../shipping-zones/shipping-zones.js | 178 +++++++++++++++++- static/js/index.js | 4 +- templates/nostrmarket/index.html | 5 +- views_api.py | 106 ++++++++++- 8 files changed, 444 insertions(+), 20 deletions(-) diff --git a/crud.py b/crud.py index 8f41bff..96c5bf0 100644 --- a/crud.py +++ b/crud.py @@ -1,10 +1,12 @@ import json -from typing import Optional +from typing import List, Optional from lnbits.helpers import urlsafe_short_hash from . import db -from .models import Merchant, PartialMerchant +from .models import Merchant, PartialMerchant, PartialZone, Zone + +######################################## MERCHANT ######################################## async def create_merchant(user_id: str, m: PartialMerchant) -> Merchant: @@ -40,3 +42,66 @@ async def get_merchant_for_user(user_id: str) -> Optional[Merchant]: ) return Merchant.from_row(row) if row else None + + +######################################## ZONES ######################################## + + +async def create_zone(user_id: str, data: PartialZone) -> Zone: + zone_id = urlsafe_short_hash() + await db.execute( + f""" + INSERT INTO nostrmarket.zones ( + id, + user_id, + name, + currency, + cost, + regions + + ) + VALUES (?, ?, ?, ?, ?, ?) + """, + ( + zone_id, + user_id, + data.name, + data.currency, + data.cost, + json.dumps(data.countries), + ), + ) + + zone = await get_zone(user_id, zone_id) + assert zone, "Newly created zone couldn't be retrieved" + return zone + + +async def update_zone(user_id: str, z: Zone) -> Optional[Zone]: + await db.execute( + f"UPDATE nostrmarket.zones SET name = ?, cost = ?, regions = ? WHERE id = ? AND user_id = ?", + (z.name, z.cost, json.dumps(z.countries), z.id, user_id), + ) + return await get_zone(user_id, z.id) + + +async def get_zone(user_id: str, zone_id: str) -> Optional[Zone]: + row = await db.fetchone( + "SELECT * FROM nostrmarket.zones WHERE user_id = ? AND id = ?", + ( + user_id, + zone_id, + ), + ) + return Zone.from_row(row) if row else None + + +async def get_zones(user_id: str) -> List[Zone]: + rows = await db.fetchall( + "SELECT * FROM nostrmarket.zones WHERE user_id = ?", (user_id,) + ) + return [Zone.from_row(row) for row in rows] + + +async def delete_zone(zone_id: str) -> None: + await db.execute("DELETE FROM nostrmarket.zones WHERE id = ?", (zone_id,)) diff --git a/migrations.py b/migrations.py index b3fafbe..04a4fe1 100644 --- a/migrations.py +++ b/migrations.py @@ -59,8 +59,9 @@ async def m001_initial(db): id TEXT PRIMARY KEY, user_id TEXT NOT NULL, name TEXT NOT NULL, + currency TEXT NOT NULL, cost REAL NOT NULL, - countries TEXT NOT NULL + regions TEXT NOT NULL DEFAULT '[]' ); """ ) diff --git a/models.py b/models.py index e6a4e5e..40bb57f 100644 --- a/models.py +++ b/models.py @@ -1,10 +1,12 @@ import json from sqlite3 import Row -from typing import Optional +from typing import List, Optional +from fastapi import Query from pydantic import BaseModel +######################################## MERCHANT ######################################## class MerchantConfig(BaseModel): name: Optional[str] @@ -23,3 +25,21 @@ class Merchant(PartialMerchant): merchant = cls(**dict(row)) merchant.config = MerchantConfig(**json.loads(row["meta"])) return merchant + + +######################################## ZONES ######################################## +class PartialZone(BaseModel): + name: Optional[str] + currency: str + cost: float + countries: List[str] = [] + + +class Zone(PartialZone): + id: str + + @classmethod + def from_row(cls, row: Row) -> "Zone": + zone = cls(**dict(row)) + zone.countries = json.loads(row["regions"]) + return zone diff --git a/static/components/shipping-zones/shipping-zones.html b/static/components/shipping-zones/shipping-zones.html index 08ced67..23b55b5 100644 --- a/static/components/shipping-zones/shipping-zones.html +++ b/static/components/shipping-zones/shipping-zones.html @@ -5,10 +5,10 @@ color="primary" icon="public" label="Shipping Zones" - @click="createShippingZone" + @click="openZoneDialog()" > - + New Shipping Zone Create a new shipping zone @@ -19,13 +19,80 @@ :key="zone.id" clickable v-close-popup - @click="editShippingZone" + @click="openZoneDialog(zone)" > - XXX - xxxxxxxxxxxxx + {{zone.name}} + {{zone.countries.join(", ")}} + + + + + + + + +
+
+ Update + Delete +
+
+ Create Shipping Zone +
+ + Cancel +
+
+
+
diff --git a/static/components/shipping-zones/shipping-zones.js b/static/components/shipping-zones/shipping-zones.js index 4aef5f8..71e3ae6 100644 --- a/static/components/shipping-zones/shipping-zones.js +++ b/static/components/shipping-zones/shipping-zones.js @@ -2,18 +2,188 @@ async function shippingZones(path) { const template = await loadTemplateAsync(path) Vue.component('shipping-zones', { name: 'shipping-zones', + props: ['adminkey', 'inkey'], template, data: function () { return { - zones: [] + zones: [], + zoneDialog: { + showDialog: false, + data: { + id: null, + name: '', + countries: [], + cost: 0, + currency: 'sat' + } + }, + currencies: [], + shippingZoneOptions: [ + 'Free (digital)', + 'Flat rate', + 'Worldwide', + 'Europe', + 'Australia', + 'Austria', + 'Belgium', + 'Brazil', + 'Canada', + 'Denmark', + 'Finland', + 'France', + 'Germany', + 'Greece', + 'Hong Kong', + 'Hungary', + 'Ireland', + 'Indonesia', + 'Israel', + 'Italy', + 'Japan', + 'Kazakhstan', + 'Korea', + 'Luxembourg', + 'Malaysia', + 'Mexico', + 'Netherlands', + 'New Zealand', + 'Norway', + 'Poland', + 'Portugal', + 'Romania', + 'Russia', + 'Saudi Arabia', + 'Singapore', + 'Spain', + 'Sweden', + 'Switzerland', + 'Thailand', + 'Turkey', + 'Ukraine', + 'United Kingdom**', + 'United States***', + 'Vietnam', + 'China' + ] } }, methods: { - createShippingZone: async function () { - console.log('### createShippingZone', createShippingZone) + openZoneDialog: function (data) { + data = data || { + id: null, + name: '', + countries: [], + cost: 0, + currency: 'sat' + } + this.zoneDialog.data = data + + this.zoneDialog.showDialog = true }, - editShippingZone: async function () {} + createZone: async function () { + try { + const {data} = await LNbits.api.request( + 'POST', + '/nostrmarket/api/v1/zone', + this.adminkey, + {} + ) + this.zones = data + } catch (error) { + LNbits.utils.notifyApiError(error) + } + }, + getZones: async function () { + try { + const {data} = await LNbits.api.request( + 'GET', + '/nostrmarket/api/v1/zone', + this.inkey + ) + this.zones = data + console.log('### data', data) + } catch (error) { + LNbits.utils.notifyApiError(error) + } + }, + sendZoneFormData: async function () { + console.log('### data', this.zoneDialog.data) + this.zoneDialog.showDialog = false + if (this.zoneDialog.data.id) { + await this.updateShippingZone(this.zoneDialog.data) + } else { + await this.createShippingZone(this.zoneDialog.data) + } + await this.getZones() + }, + createShippingZone: async function (newZone) { + console.log('### newZone', newZone) + try { + await LNbits.api.request( + 'POST', + '/nostrmarket/api/v1/zone', + this.adminkey, + newZone + ) + this.$q.notify({ + type: 'positive', + message: 'Zone created!' + }) + } catch (error) { + LNbits.utils.notifyApiError(error) + } + }, + updateShippingZone: async function (updatedZone) { + try { + await LNbits.api.request( + 'PATCH', + `/nostrmarket/api/v1/zone/${updatedZone.id}`, + this.adminkey, + updatedZone + ) + this.$q.notify({ + type: 'positive', + message: 'Zone updated!' + }) + } catch (error) { + LNbits.utils.notifyApiError(error) + } + }, + deleteShippingZone: async function () { + try { + await LNbits.api.request( + 'DELETE', + `/nostrmarket/api/v1/zone/${this.zoneDialog.data.id}`, + this.adminkey + ) + this.$q.notify({ + type: 'positive', + message: 'Zone deleted!' + }) + await this.getZones() + this.zoneDialog.showDialog = false + } catch (error) { + LNbits.utils.notifyApiError(error) + } + }, + async getCurrencies() { + try { + const {data} = await LNbits.api.request( + 'GET', + '/nostrmarket/api/v1/currencies', + this.inkey + ) + + this.currencies = ['sat', ...data] + } catch (error) { + LNbits.utils.notifyApiError(error) + } + } + }, + created: async function () { + await this.getZones() + await this.getCurrencies() } }) } diff --git a/static/js/index.js b/static/js/index.js index 6d55b30..68ece51 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -42,9 +42,9 @@ const merchant = async () => { getMerchant: async function () { try { const {data} = await LNbits.api.request( - 'get', + 'GET', '/nostrmarket/api/v1/merchant', - this.g.user.wallets[0].adminkey + this.g.user.wallets[0].inkey ) this.merchant = data } catch (error) { diff --git a/templates/nostrmarket/index.html b/templates/nostrmarket/index.html index f7aaa6c..b4378d8 100644 --- a/templates/nostrmarket/index.html +++ b/templates/nostrmarket/index.html @@ -63,7 +63,10 @@
- +
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 create merchant", + ) + + +@nostrmarket_ext.post("/api/v1/zone") +async def api_create_zone( + data: PartialZone, wallet: WalletTypeInfo = Depends(get_key_type) +): + 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 merchant", + ) + + +@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 create merchant", + ) + + +@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 create merchant", + ) + + +@nostrmarket_ext.get("/api/v1/currencies") +async def api_list_currencies_available(): + return list(currencies.keys()) From fef4f349f2124dbee6586ffed81f6599f2d8b96a Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Tue, 28 Feb 2023 16:30:09 +0200 Subject: [PATCH 036/891] feat: manage shipping zones --- crud.py | 69 ++++++- migrations.py | 3 +- models.py | 22 ++- .../shipping-zones/shipping-zones.html | 77 +++++++- .../shipping-zones/shipping-zones.js | 178 +++++++++++++++++- static/js/index.js | 4 +- templates/nostrmarket/index.html | 5 +- views_api.py | 106 ++++++++++- 8 files changed, 444 insertions(+), 20 deletions(-) diff --git a/crud.py b/crud.py index 8f41bff..96c5bf0 100644 --- a/crud.py +++ b/crud.py @@ -1,10 +1,12 @@ import json -from typing import Optional +from typing import List, Optional from lnbits.helpers import urlsafe_short_hash from . import db -from .models import Merchant, PartialMerchant +from .models import Merchant, PartialMerchant, PartialZone, Zone + +######################################## MERCHANT ######################################## async def create_merchant(user_id: str, m: PartialMerchant) -> Merchant: @@ -40,3 +42,66 @@ async def get_merchant_for_user(user_id: str) -> Optional[Merchant]: ) return Merchant.from_row(row) if row else None + + +######################################## ZONES ######################################## + + +async def create_zone(user_id: str, data: PartialZone) -> Zone: + zone_id = urlsafe_short_hash() + await db.execute( + f""" + INSERT INTO nostrmarket.zones ( + id, + user_id, + name, + currency, + cost, + regions + + ) + VALUES (?, ?, ?, ?, ?, ?) + """, + ( + zone_id, + user_id, + data.name, + data.currency, + data.cost, + json.dumps(data.countries), + ), + ) + + zone = await get_zone(user_id, zone_id) + assert zone, "Newly created zone couldn't be retrieved" + return zone + + +async def update_zone(user_id: str, z: Zone) -> Optional[Zone]: + await db.execute( + f"UPDATE nostrmarket.zones SET name = ?, cost = ?, regions = ? WHERE id = ? AND user_id = ?", + (z.name, z.cost, json.dumps(z.countries), z.id, user_id), + ) + return await get_zone(user_id, z.id) + + +async def get_zone(user_id: str, zone_id: str) -> Optional[Zone]: + row = await db.fetchone( + "SELECT * FROM nostrmarket.zones WHERE user_id = ? AND id = ?", + ( + user_id, + zone_id, + ), + ) + return Zone.from_row(row) if row else None + + +async def get_zones(user_id: str) -> List[Zone]: + rows = await db.fetchall( + "SELECT * FROM nostrmarket.zones WHERE user_id = ?", (user_id,) + ) + return [Zone.from_row(row) for row in rows] + + +async def delete_zone(zone_id: str) -> None: + await db.execute("DELETE FROM nostrmarket.zones WHERE id = ?", (zone_id,)) diff --git a/migrations.py b/migrations.py index b3fafbe..04a4fe1 100644 --- a/migrations.py +++ b/migrations.py @@ -59,8 +59,9 @@ async def m001_initial(db): id TEXT PRIMARY KEY, user_id TEXT NOT NULL, name TEXT NOT NULL, + currency TEXT NOT NULL, cost REAL NOT NULL, - countries TEXT NOT NULL + regions TEXT NOT NULL DEFAULT '[]' ); """ ) diff --git a/models.py b/models.py index e6a4e5e..40bb57f 100644 --- a/models.py +++ b/models.py @@ -1,10 +1,12 @@ import json from sqlite3 import Row -from typing import Optional +from typing import List, Optional +from fastapi import Query from pydantic import BaseModel +######################################## MERCHANT ######################################## class MerchantConfig(BaseModel): name: Optional[str] @@ -23,3 +25,21 @@ class Merchant(PartialMerchant): merchant = cls(**dict(row)) merchant.config = MerchantConfig(**json.loads(row["meta"])) return merchant + + +######################################## ZONES ######################################## +class PartialZone(BaseModel): + name: Optional[str] + currency: str + cost: float + countries: List[str] = [] + + +class Zone(PartialZone): + id: str + + @classmethod + def from_row(cls, row: Row) -> "Zone": + zone = cls(**dict(row)) + zone.countries = json.loads(row["regions"]) + return zone diff --git a/static/components/shipping-zones/shipping-zones.html b/static/components/shipping-zones/shipping-zones.html index 08ced67..23b55b5 100644 --- a/static/components/shipping-zones/shipping-zones.html +++ b/static/components/shipping-zones/shipping-zones.html @@ -5,10 +5,10 @@ color="primary" icon="public" label="Shipping Zones" - @click="createShippingZone" + @click="openZoneDialog()" > - + New Shipping Zone Create a new shipping zone @@ -19,13 +19,80 @@ :key="zone.id" clickable v-close-popup - @click="editShippingZone" + @click="openZoneDialog(zone)" > - XXX - xxxxxxxxxxxxx + {{zone.name}} + {{zone.countries.join(", ")}} + + + + + + + + +
+
+ Update + Delete +
+
+ Create Shipping Zone +
+ + Cancel +
+
+
+
diff --git a/static/components/shipping-zones/shipping-zones.js b/static/components/shipping-zones/shipping-zones.js index 4aef5f8..71e3ae6 100644 --- a/static/components/shipping-zones/shipping-zones.js +++ b/static/components/shipping-zones/shipping-zones.js @@ -2,18 +2,188 @@ async function shippingZones(path) { const template = await loadTemplateAsync(path) Vue.component('shipping-zones', { name: 'shipping-zones', + props: ['adminkey', 'inkey'], template, data: function () { return { - zones: [] + zones: [], + zoneDialog: { + showDialog: false, + data: { + id: null, + name: '', + countries: [], + cost: 0, + currency: 'sat' + } + }, + currencies: [], + shippingZoneOptions: [ + 'Free (digital)', + 'Flat rate', + 'Worldwide', + 'Europe', + 'Australia', + 'Austria', + 'Belgium', + 'Brazil', + 'Canada', + 'Denmark', + 'Finland', + 'France', + 'Germany', + 'Greece', + 'Hong Kong', + 'Hungary', + 'Ireland', + 'Indonesia', + 'Israel', + 'Italy', + 'Japan', + 'Kazakhstan', + 'Korea', + 'Luxembourg', + 'Malaysia', + 'Mexico', + 'Netherlands', + 'New Zealand', + 'Norway', + 'Poland', + 'Portugal', + 'Romania', + 'Russia', + 'Saudi Arabia', + 'Singapore', + 'Spain', + 'Sweden', + 'Switzerland', + 'Thailand', + 'Turkey', + 'Ukraine', + 'United Kingdom**', + 'United States***', + 'Vietnam', + 'China' + ] } }, methods: { - createShippingZone: async function () { - console.log('### createShippingZone', createShippingZone) + openZoneDialog: function (data) { + data = data || { + id: null, + name: '', + countries: [], + cost: 0, + currency: 'sat' + } + this.zoneDialog.data = data + + this.zoneDialog.showDialog = true }, - editShippingZone: async function () {} + createZone: async function () { + try { + const {data} = await LNbits.api.request( + 'POST', + '/nostrmarket/api/v1/zone', + this.adminkey, + {} + ) + this.zones = data + } catch (error) { + LNbits.utils.notifyApiError(error) + } + }, + getZones: async function () { + try { + const {data} = await LNbits.api.request( + 'GET', + '/nostrmarket/api/v1/zone', + this.inkey + ) + this.zones = data + console.log('### data', data) + } catch (error) { + LNbits.utils.notifyApiError(error) + } + }, + sendZoneFormData: async function () { + console.log('### data', this.zoneDialog.data) + this.zoneDialog.showDialog = false + if (this.zoneDialog.data.id) { + await this.updateShippingZone(this.zoneDialog.data) + } else { + await this.createShippingZone(this.zoneDialog.data) + } + await this.getZones() + }, + createShippingZone: async function (newZone) { + console.log('### newZone', newZone) + try { + await LNbits.api.request( + 'POST', + '/nostrmarket/api/v1/zone', + this.adminkey, + newZone + ) + this.$q.notify({ + type: 'positive', + message: 'Zone created!' + }) + } catch (error) { + LNbits.utils.notifyApiError(error) + } + }, + updateShippingZone: async function (updatedZone) { + try { + await LNbits.api.request( + 'PATCH', + `/nostrmarket/api/v1/zone/${updatedZone.id}`, + this.adminkey, + updatedZone + ) + this.$q.notify({ + type: 'positive', + message: 'Zone updated!' + }) + } catch (error) { + LNbits.utils.notifyApiError(error) + } + }, + deleteShippingZone: async function () { + try { + await LNbits.api.request( + 'DELETE', + `/nostrmarket/api/v1/zone/${this.zoneDialog.data.id}`, + this.adminkey + ) + this.$q.notify({ + type: 'positive', + message: 'Zone deleted!' + }) + await this.getZones() + this.zoneDialog.showDialog = false + } catch (error) { + LNbits.utils.notifyApiError(error) + } + }, + async getCurrencies() { + try { + const {data} = await LNbits.api.request( + 'GET', + '/nostrmarket/api/v1/currencies', + this.inkey + ) + + this.currencies = ['sat', ...data] + } catch (error) { + LNbits.utils.notifyApiError(error) + } + } + }, + created: async function () { + await this.getZones() + await this.getCurrencies() } }) } diff --git a/static/js/index.js b/static/js/index.js index 6d55b30..68ece51 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -42,9 +42,9 @@ const merchant = async () => { getMerchant: async function () { try { const {data} = await LNbits.api.request( - 'get', + 'GET', '/nostrmarket/api/v1/merchant', - this.g.user.wallets[0].adminkey + this.g.user.wallets[0].inkey ) this.merchant = data } catch (error) { diff --git a/templates/nostrmarket/index.html b/templates/nostrmarket/index.html index f7aaa6c..b4378d8 100644 --- a/templates/nostrmarket/index.html +++ b/templates/nostrmarket/index.html @@ -63,7 +63,10 @@
- +
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 create merchant", + ) + + +@nostrmarket_ext.post("/api/v1/zone") +async def api_create_zone( + data: PartialZone, wallet: WalletTypeInfo = Depends(get_key_type) +): + 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 merchant", + ) + + +@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 create merchant", + ) + + +@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 create merchant", + ) + + +@nostrmarket_ext.get("/api/v1/currencies") +async def api_list_currencies_available(): + return list(currencies.keys()) From fd480bf305a33d81921528c777d164e10ca23d20 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Tue, 28 Feb 2023 16:37:18 +0200 Subject: [PATCH 037/891] fix: do not edit currency for zone --- static/components/shipping-zones/shipping-zones.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/components/shipping-zones/shipping-zones.html b/static/components/shipping-zones/shipping-zones.html index 23b55b5..100afe9 100644 --- a/static/components/shipping-zones/shipping-zones.html +++ b/static/components/shipping-zones/shipping-zones.html @@ -48,7 +48,7 @@ v-model="zoneDialog.data.countries" > Date: Tue, 28 Feb 2023 16:37:18 +0200 Subject: [PATCH 038/891] fix: do not edit currency for zone --- static/components/shipping-zones/shipping-zones.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/components/shipping-zones/shipping-zones.html b/static/components/shipping-zones/shipping-zones.html index 23b55b5..100afe9 100644 --- a/static/components/shipping-zones/shipping-zones.html +++ b/static/components/shipping-zones/shipping-zones.html @@ -48,7 +48,7 @@ v-model="zoneDialog.data.countries" > Date: Tue, 28 Feb 2023 14:45:45 +0000 Subject: [PATCH 039/891] initial files --- templates/nostrmarket/market.html | 202 ++++++++++++++++++++++++++++++ templates/nostrmarket/stall.html | 61 +++++++++ views.py | 18 +++ 3 files changed, 281 insertions(+) create mode 100644 templates/nostrmarket/market.html create mode 100644 templates/nostrmarket/stall.html diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html new file mode 100644 index 0000000..ef29755 --- /dev/null +++ b/templates/nostrmarket/market.html @@ -0,0 +1,202 @@ +{% extends "public.html" %} {% block page %} +
+
+ +
+ Market: +
+
+ + + +
+
+
+
+
+
+ + {% 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/templates/nostrmarket/stall.html b/templates/nostrmarket/stall.html new file mode 100644 index 0000000..208938c --- /dev/null +++ b/templates/nostrmarket/stall.html @@ -0,0 +1,61 @@ +{% extends "public.html" %} {% block page %} +
+{% endblock %} {% block scripts %} + + +{% endblock %} diff --git a/views.py b/views.py index ca8e1f7..47b07ff 100644 --- a/views.py +++ b/views.py @@ -20,3 +20,21 @@ async def index(request: Request, user: User = Depends(check_user_exists)): "nostrmarket/index.html", {"request": request, "user": user.dict()}, ) + + +@nostrmarket_ext.get("/market", response_class=HTMLResponse) +async def market(request: Request): + return nostrmarket_renderer().TemplateResponse( + "nostrmarket/market.html", + { + "request": request, + }, + ) + + +@nostrmarket_ext.get("/stall/{stall_id}", response_class=HTMLResponse) +async def stall(request: Request, stall_id: str): + return nostrmarket_renderer().TemplateResponse( + "nostrmarket/stall.html", + {"request": request, "stall_id": stall_id}, + ) From c7254b16ee435e19d5102d366b935a3923f34bbe Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Tue, 28 Feb 2023 14:45:45 +0000 Subject: [PATCH 040/891] initial files --- templates/nostrmarket/market.html | 202 ++++++++++++++++++++++++++++++ templates/nostrmarket/stall.html | 61 +++++++++ views.py | 18 +++ 3 files changed, 281 insertions(+) create mode 100644 templates/nostrmarket/market.html create mode 100644 templates/nostrmarket/stall.html diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html new file mode 100644 index 0000000..ef29755 --- /dev/null +++ b/templates/nostrmarket/market.html @@ -0,0 +1,202 @@ +{% extends "public.html" %} {% block page %} +
+
+ +
+ Market: +
+
+ + + +
+
+
+
+
+
+ + {% 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/templates/nostrmarket/stall.html b/templates/nostrmarket/stall.html new file mode 100644 index 0000000..208938c --- /dev/null +++ b/templates/nostrmarket/stall.html @@ -0,0 +1,61 @@ +{% extends "public.html" %} {% block page %} +
+{% endblock %} {% block scripts %} + + +{% endblock %} diff --git a/views.py b/views.py index ca8e1f7..47b07ff 100644 --- a/views.py +++ b/views.py @@ -20,3 +20,21 @@ async def index(request: Request, user: User = Depends(check_user_exists)): "nostrmarket/index.html", {"request": request, "user": user.dict()}, ) + + +@nostrmarket_ext.get("/market", response_class=HTMLResponse) +async def market(request: Request): + return nostrmarket_renderer().TemplateResponse( + "nostrmarket/market.html", + { + "request": request, + }, + ) + + +@nostrmarket_ext.get("/stall/{stall_id}", response_class=HTMLResponse) +async def stall(request: Request, stall_id: str): + return nostrmarket_renderer().TemplateResponse( + "nostrmarket/stall.html", + {"request": request, "stall_id": stall_id}, + ) From 59da80420788a571896a209a64601311754eb3ee Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Tue, 28 Feb 2023 16:00:57 +0000 Subject: [PATCH 041/891] get nostr events and display --- templates/nostrmarket/market.html | 299 ++++++++++++++++++------------ 1 file changed, 182 insertions(+), 117 deletions(-) diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html index ef29755..eb18f39 100644 --- a/templates/nostrmarket/market.html +++ b/templates/nostrmarket/market.html @@ -1,113 +1,162 @@ {% extends "public.html" %} {% block page %} -
-
- -
- Market: -
-
- - - -
+ + + + Settings -
-
-
-
- - {% 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 }} - + + Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quidem, + eius reprehenderit eos corrupti commodi magni quaerat ex numquam, + dolorum officiis modi facere maiores architecto suscipit iste + eveniet doloribus ullam aliquid. + +
+ + - Visit Stall - - - {% endraw %} - -
-
+ + + Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quidem, + eius reprehenderit eos corrupti commodi magni quaerat ex numquam, + dolorum officiis modi facere maiores architecto suscipit iste + eveniet doloribus ullam aliquid. + + + + +
+ + +
+
+ +
+ Market: +
+
+ + + +
+
+
+
+
+
+ + {% 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 %} From 7d273248a55b93e9feaee2059c0cb9de27b68117 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Tue, 28 Feb 2023 16:00:57 +0000 Subject: [PATCH 042/891] get nostr events and display --- templates/nostrmarket/market.html | 299 ++++++++++++++++++------------ 1 file changed, 182 insertions(+), 117 deletions(-) diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html index ef29755..eb18f39 100644 --- a/templates/nostrmarket/market.html +++ b/templates/nostrmarket/market.html @@ -1,113 +1,162 @@ {% extends "public.html" %} {% block page %} -
-
- -
- Market: -
-
- - - -
+ + + + Settings -
-
-
-
- - {% 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 }} - + + Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quidem, + eius reprehenderit eos corrupti commodi magni quaerat ex numquam, + dolorum officiis modi facere maiores architecto suscipit iste + eveniet doloribus ullam aliquid. + +
+ + - Visit Stall - - - {% endraw %} - -
-
+ + + Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quidem, + eius reprehenderit eos corrupti commodi magni quaerat ex numquam, + dolorum officiis modi facere maiores architecto suscipit iste + eveniet doloribus ullam aliquid. + + + + +
+ + +
+
+ +
+ Market: +
+
+ + + +
+
+
+
+
+
+ + {% 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 %} From cd026a0a0b2e6b982e39936acedd5b300eddc076 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Tue, 28 Feb 2023 17:06:24 +0000 Subject: [PATCH 043/891] menu add pubkey/npub --- templates/nostrmarket/market.html | 120 +++++++++++++++++++++++++++--- 1 file changed, 108 insertions(+), 12 deletions(-) diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html index eb18f39..33af581 100644 --- a/templates/nostrmarket/market.html +++ b/templates/nostrmarket/market.html @@ -14,10 +14,44 @@ > - Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quidem, - eius reprehenderit eos corrupti commodi magni quaerat ex numquam, - dolorum officiis modi facere maiores architecto suscipit iste - eveniet doloribus ullam aliquid. + + + + + + + + + + + {%raw%} + + {{ `${pub.slice(0, 5)}...${pub.slice(-5)}` }}{{ pub }} + + + + + {%endraw%} + + @@ -29,10 +63,35 @@ > - Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quidem, - eius reprehenderit eos corrupti commodi magni quaerat ex numquam, - dolorum officiis modi facere maiores architecto suscipit iste - eveniet doloribus ullam aliquid. + + + + + + {%raw%} + + {{ url }} + + + + + {%endraw%} + + @@ -190,11 +249,14 @@ return { drawer: true, pubkeys: new Set(), + relays: new Set(defaultRelays), stalls: [], products: [], events: [], searchText: null, - exchangeRates: null + exchangeRates: null, + inputPubkey: null, + inputRelay: null } }, computed: { @@ -220,7 +282,7 @@ async initNostr() { this.pool = new nostr.SimplePool() this.relays = new Set(defaultRelays) - await this.pool + let sub = await this.pool .list(Array.from(this.relays), [ { kinds: [30005], @@ -236,11 +298,12 @@ } else { // it's a stall this.stalls.push(e.content) + return } }) - console.log(this.stalls) - console.log(this.products) }) + await Promise.resolve(sub) + this.pool.close() }, async getRates() { let noFiat = this.stalls.map(s => s.currency).every(c => c == 'sat') @@ -260,8 +323,41 @@ }, getAmountFormated(amount, unit = 'USD') { return LNbits.utils.formatCurrency(amount, unit) + }, + addPubkey() { + let pubkey = String(this.inputPubkey).trim() + let regExp = /^#([0-9a-f]{3}){1,2}$/i + if (regExp.test(pubkey)) { + return this.pubkeys.add(pubkey) + } + try { + let {type, data} = nostr.nip19.decode(pubkey) + if (type === 'npub') pubkey = data + else if (type === 'nprofile') { + pubkey = data.pubkey + givenRelays = data.relays + } + this.pubkeys.add(pubkey) + this.inputPubkey = null + } catch (err) { + console.error(err) + } + }, + addRelay() { + let relay = String(this.inputRelay).trim() + if (!relay.startsWith('ws')) { + console.debug('invalid url') + return + } + this.relays.add(relay) + this.inputRelay = null } } }) + {% endblock %} From eac1a77781a40afd42f4c8d96657073f5b71c246 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Tue, 28 Feb 2023 17:06:24 +0000 Subject: [PATCH 044/891] menu add pubkey/npub --- templates/nostrmarket/market.html | 120 +++++++++++++++++++++++++++--- 1 file changed, 108 insertions(+), 12 deletions(-) diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html index eb18f39..33af581 100644 --- a/templates/nostrmarket/market.html +++ b/templates/nostrmarket/market.html @@ -14,10 +14,44 @@ > - Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quidem, - eius reprehenderit eos corrupti commodi magni quaerat ex numquam, - dolorum officiis modi facere maiores architecto suscipit iste - eveniet doloribus ullam aliquid. + + + + + + + + + + + {%raw%} + + {{ `${pub.slice(0, 5)}...${pub.slice(-5)}` }}{{ pub }} + + + + + {%endraw%} + + @@ -29,10 +63,35 @@ > - Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quidem, - eius reprehenderit eos corrupti commodi magni quaerat ex numquam, - dolorum officiis modi facere maiores architecto suscipit iste - eveniet doloribus ullam aliquid. + + + + + + {%raw%} + + {{ url }} + + + + + {%endraw%} + + @@ -190,11 +249,14 @@ return { drawer: true, pubkeys: new Set(), + relays: new Set(defaultRelays), stalls: [], products: [], events: [], searchText: null, - exchangeRates: null + exchangeRates: null, + inputPubkey: null, + inputRelay: null } }, computed: { @@ -220,7 +282,7 @@ async initNostr() { this.pool = new nostr.SimplePool() this.relays = new Set(defaultRelays) - await this.pool + let sub = await this.pool .list(Array.from(this.relays), [ { kinds: [30005], @@ -236,11 +298,12 @@ } else { // it's a stall this.stalls.push(e.content) + return } }) - console.log(this.stalls) - console.log(this.products) }) + await Promise.resolve(sub) + this.pool.close() }, async getRates() { let noFiat = this.stalls.map(s => s.currency).every(c => c == 'sat') @@ -260,8 +323,41 @@ }, getAmountFormated(amount, unit = 'USD') { return LNbits.utils.formatCurrency(amount, unit) + }, + addPubkey() { + let pubkey = String(this.inputPubkey).trim() + let regExp = /^#([0-9a-f]{3}){1,2}$/i + if (regExp.test(pubkey)) { + return this.pubkeys.add(pubkey) + } + try { + let {type, data} = nostr.nip19.decode(pubkey) + if (type === 'npub') pubkey = data + else if (type === 'nprofile') { + pubkey = data.pubkey + givenRelays = data.relays + } + this.pubkeys.add(pubkey) + this.inputPubkey = null + } catch (err) { + console.error(err) + } + }, + addRelay() { + let relay = String(this.inputRelay).trim() + if (!relay.startsWith('ws')) { + console.debug('invalid url') + return + } + this.relays.add(relay) + this.inputRelay = null } } }) + {% endblock %} From 5b1f83d6f8f49562f15df227b8db3dba76657990 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Tue, 28 Feb 2023 21:04:29 +0000 Subject: [PATCH 045/891] remove pubkeys and relays --- templates/nostrmarket/market.html | 36 ++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html index 33af581..103d7db 100644 --- a/templates/nostrmarket/market.html +++ b/templates/nostrmarket/market.html @@ -1,6 +1,6 @@ {% extends "public.html" %} {% block page %} - - + + Settings @@ -47,6 +47,7 @@ dense round icon="delete" + @click="removePubkey(pub)" /> {%endraw%} @@ -87,6 +88,7 @@ dense round icon="delete" + @click="removeRelay(url)" /> {%endraw%} @@ -212,7 +214,7 @@
- + @@ -247,9 +249,9 @@ mixins: [windowMixin], data: function () { return { - drawer: true, + drawer: false, pubkeys: new Set(), - relays: new Set(defaultRelays), + relays: new Set(), stalls: [], products: [], events: [], @@ -269,6 +271,9 @@ p.categories.includes(this.searchText) ) }) + }, + relayList() { + return Array.from(this.relays) } }, async created() { @@ -276,12 +281,12 @@ this.pubkeys.add( '855ea22a88d7df7ccd8497777db81f115575d5362f51df3af02ead383f5eaba2' ) - //await this.initNostr() + this.relays = new Set(defaultRelays) + await this.initNostr() }, methods: { async initNostr() { this.pool = new nostr.SimplePool() - this.relays = new Set(defaultRelays) let sub = await this.pool .list(Array.from(this.relays), [ { @@ -343,6 +348,12 @@ console.error(err) } }, + removePubkey(pubkey) { + // Needs a hack for Vue reactivity + let pubkeys = this.pubkeys + pubkeys.delete(pubkey) + this.pubkeys = new Set(Array.from(pubkeys)) + }, addRelay() { let relay = String(this.inputRelay).trim() if (!relay.startsWith('ws')) { @@ -351,13 +362,14 @@ } this.relays.add(relay) this.inputRelay = null + }, + removeRelay(relay) { + // Needs a hack for Vue reactivity + let relays = this.relays + relays.delete(relay) + this.relays = new Set(Array.from(relays)) } } }) - {% endblock %} From 19001745036dbf3cd92fb4b6fe05e12a3e3d01d7 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Tue, 28 Feb 2023 21:04:29 +0000 Subject: [PATCH 046/891] remove pubkeys and relays --- templates/nostrmarket/market.html | 36 ++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html index 33af581..103d7db 100644 --- a/templates/nostrmarket/market.html +++ b/templates/nostrmarket/market.html @@ -1,6 +1,6 @@ {% extends "public.html" %} {% block page %} - - + + Settings @@ -47,6 +47,7 @@ dense round icon="delete" + @click="removePubkey(pub)" /> {%endraw%} @@ -87,6 +88,7 @@ dense round icon="delete" + @click="removeRelay(url)" /> {%endraw%} @@ -212,7 +214,7 @@
- + @@ -247,9 +249,9 @@ mixins: [windowMixin], data: function () { return { - drawer: true, + drawer: false, pubkeys: new Set(), - relays: new Set(defaultRelays), + relays: new Set(), stalls: [], products: [], events: [], @@ -269,6 +271,9 @@ p.categories.includes(this.searchText) ) }) + }, + relayList() { + return Array.from(this.relays) } }, async created() { @@ -276,12 +281,12 @@ this.pubkeys.add( '855ea22a88d7df7ccd8497777db81f115575d5362f51df3af02ead383f5eaba2' ) - //await this.initNostr() + this.relays = new Set(defaultRelays) + await this.initNostr() }, methods: { async initNostr() { this.pool = new nostr.SimplePool() - this.relays = new Set(defaultRelays) let sub = await this.pool .list(Array.from(this.relays), [ { @@ -343,6 +348,12 @@ console.error(err) } }, + removePubkey(pubkey) { + // Needs a hack for Vue reactivity + let pubkeys = this.pubkeys + pubkeys.delete(pubkey) + this.pubkeys = new Set(Array.from(pubkeys)) + }, addRelay() { let relay = String(this.inputRelay).trim() if (!relay.startsWith('ws')) { @@ -351,13 +362,14 @@ } this.relays.add(relay) this.inputRelay = null + }, + removeRelay(relay) { + // Needs a hack for Vue reactivity + let relays = this.relays + relays.delete(relay) + this.relays = new Set(Array.from(relays)) } } }) - {% endblock %} From f9f8b658433a2c9b4dffb3b132f1a672d3c1531e Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Tue, 28 Feb 2023 22:38:50 +0000 Subject: [PATCH 047/891] fetch profiles --- templates/nostrmarket/market.html | 104 +++++++++++++++++++----------- 1 file changed, 66 insertions(+), 38 deletions(-) diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html index 103d7db..a85f2fc 100644 --- a/templates/nostrmarket/market.html +++ b/templates/nostrmarket/market.html @@ -24,20 +24,31 @@ > - + + {%raw%} - + + - {%raw%} {{ `${pub.slice(0, 5)}...${pub.slice(-5)}` }}{{ pub }}{{ profiles.get(pub).name }} + {{ `${pub.slice(0, 5)}...${pub.slice(-5)}` + }} + {{ pub }} {% raw %} { this.events = events || [] this.events.map(eventToObj).map(e => { - if (e.content.stall) { - //it's a product - this.products.push(e.content) + if (e.kind == 0) { + this.profiles.set(e.pubkey, e.content) + return + } else if (e.content.stall) { + //it's a product `d` is the prod. id + products.set(e.d, e.content) } else { - // it's a stall - this.stalls.push(e.content) + // it's a stall `d` is the stall id + stalls.set(e.d, e.content) return } }) }) await Promise.resolve(sub) - this.pool.close() + this.products = Array.from(products.values()) + this.stalls = Array.from(stalls.values()) + pool.close(relays) }, async getRates() { let noFiat = this.stalls.map(s => s.currency).every(c => c == 'sat') @@ -329,32 +352,36 @@ getAmountFormated(amount, unit = 'USD') { return LNbits.utils.formatCurrency(amount, unit) }, - addPubkey() { + async addPubkey() { let pubkey = String(this.inputPubkey).trim() let regExp = /^#([0-9a-f]{3}){1,2}$/i - if (regExp.test(pubkey)) { - return this.pubkeys.add(pubkey) - } - try { - let {type, data} = nostr.nip19.decode(pubkey) - if (type === 'npub') pubkey = data - else if (type === 'nprofile') { - pubkey = data.pubkey - givenRelays = data.relays + if (pubkey.startsWith('n')) { + try { + let {type, data} = nostr.nip19.decode(pubkey) + if (type === 'npub') pubkey = data + else if (type === 'nprofile') { + pubkey = data.pubkey + givenRelays = data.relays + } + this.pubkeys.add(pubkey) + this.inputPubkey = null + } catch (err) { + console.error(err) } - this.pubkeys.add(pubkey) - this.inputPubkey = null - } catch (err) { - console.error(err) + } else if (regExp.test(pubkey)) { + pubkey = pubkey } + this.pubkeys.add(pubkey) + await this.initNostr() }, removePubkey(pubkey) { // Needs a hack for Vue reactivity let pubkeys = this.pubkeys pubkeys.delete(pubkey) + this.profiles.delete(pubkey) this.pubkeys = new Set(Array.from(pubkeys)) }, - addRelay() { + async addRelay() { let relay = String(this.inputRelay).trim() if (!relay.startsWith('ws')) { console.debug('invalid url') @@ -362,6 +389,7 @@ } this.relays.add(relay) this.inputRelay = null + await this.initNostr() }, removeRelay(relay) { // Needs a hack for Vue reactivity From 63c7476e6f7723c2c31e28ee9661c5174273b30b Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Tue, 28 Feb 2023 22:38:50 +0000 Subject: [PATCH 048/891] fetch profiles --- templates/nostrmarket/market.html | 104 +++++++++++++++++++----------- 1 file changed, 66 insertions(+), 38 deletions(-) diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html index 103d7db..a85f2fc 100644 --- a/templates/nostrmarket/market.html +++ b/templates/nostrmarket/market.html @@ -24,20 +24,31 @@ > - + + {%raw%} - + + - {%raw%} {{ `${pub.slice(0, 5)}...${pub.slice(-5)}` }}{{ pub }}{{ profiles.get(pub).name }} + {{ `${pub.slice(0, 5)}...${pub.slice(-5)}` + }} + {{ pub }} {% raw %} { this.events = events || [] this.events.map(eventToObj).map(e => { - if (e.content.stall) { - //it's a product - this.products.push(e.content) + if (e.kind == 0) { + this.profiles.set(e.pubkey, e.content) + return + } else if (e.content.stall) { + //it's a product `d` is the prod. id + products.set(e.d, e.content) } else { - // it's a stall - this.stalls.push(e.content) + // it's a stall `d` is the stall id + stalls.set(e.d, e.content) return } }) }) await Promise.resolve(sub) - this.pool.close() + this.products = Array.from(products.values()) + this.stalls = Array.from(stalls.values()) + pool.close(relays) }, async getRates() { let noFiat = this.stalls.map(s => s.currency).every(c => c == 'sat') @@ -329,32 +352,36 @@ getAmountFormated(amount, unit = 'USD') { return LNbits.utils.formatCurrency(amount, unit) }, - addPubkey() { + async addPubkey() { let pubkey = String(this.inputPubkey).trim() let regExp = /^#([0-9a-f]{3}){1,2}$/i - if (regExp.test(pubkey)) { - return this.pubkeys.add(pubkey) - } - try { - let {type, data} = nostr.nip19.decode(pubkey) - if (type === 'npub') pubkey = data - else if (type === 'nprofile') { - pubkey = data.pubkey - givenRelays = data.relays + if (pubkey.startsWith('n')) { + try { + let {type, data} = nostr.nip19.decode(pubkey) + if (type === 'npub') pubkey = data + else if (type === 'nprofile') { + pubkey = data.pubkey + givenRelays = data.relays + } + this.pubkeys.add(pubkey) + this.inputPubkey = null + } catch (err) { + console.error(err) } - this.pubkeys.add(pubkey) - this.inputPubkey = null - } catch (err) { - console.error(err) + } else if (regExp.test(pubkey)) { + pubkey = pubkey } + this.pubkeys.add(pubkey) + await this.initNostr() }, removePubkey(pubkey) { // Needs a hack for Vue reactivity let pubkeys = this.pubkeys pubkeys.delete(pubkey) + this.profiles.delete(pubkey) this.pubkeys = new Set(Array.from(pubkeys)) }, - addRelay() { + async addRelay() { let relay = String(this.inputRelay).trim() if (!relay.startsWith('ws')) { console.debug('invalid url') @@ -362,6 +389,7 @@ } this.relays.add(relay) this.inputRelay = null + await this.initNostr() }, removeRelay(relay) { // Needs a hack for Vue reactivity From 3733b24b8d2ebd5f35f7981102a116f3942ee75e Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Tue, 28 Feb 2023 22:39:05 +0000 Subject: [PATCH 049/891] avatar picture --- static/images/blank-avatar.webp | Bin 0 -> 11090 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 static/images/blank-avatar.webp diff --git a/static/images/blank-avatar.webp b/static/images/blank-avatar.webp new file mode 100644 index 0000000000000000000000000000000000000000..513b0f3279d36569ead8f6553506c30fbb610703 GIT binary patch literal 11090 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 ASO5S3 literal 0 HcmV?d00001 From b0eca2b1db5be38523c9ed02550e10d0464ab915 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Tue, 28 Feb 2023 22:39:05 +0000 Subject: [PATCH 050/891] avatar picture --- static/images/blank-avatar.webp | Bin 0 -> 11090 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 static/images/blank-avatar.webp diff --git a/static/images/blank-avatar.webp b/static/images/blank-avatar.webp new file mode 100644 index 0000000000000000000000000000000000000000..513b0f3279d36569ead8f6553506c30fbb610703 GIT binary patch literal 11090 zcmWIYbaV64W?%?+bqWXzu!!JdU|^77U|{&q2ttlN0UkMm3=E7ithu*68r z!B9En(hteF2EL0fXJ+1!I_v8Zv2AVFmN#>iJdJn`OCS&h_{I{5$n;imLx7PTzY|+2I%3@ilMFb7)p&~L+3$XQuWDWKxv#&<-C0z( zO;qum#A#Ojwe^F4X`5Ek{k!WAPkMiJ{nO5%!xnwbkFI}mQCs_U$&0Nv6YI>5ow40N zO?^{D_Vju2r-kS5w|X9?cze-0iA|O7w+m0r=$6v+`X+FwZrP`tm&*h7A`Fi`{>J>e zY(w_T^tsb=6z$&G2fgoS-!2yE6ZQ9%^BHTCD^V$1-S|)P3q3zPu{>kO))UXxSZ-f% zC1>va&wRXFr`@%-o^LGq+U%sY$(g9W2eFr$q_%9-zPt1IgX_g9SJEC@_e+?)yfo|g z9%tuus_!l>H8q@fTroDP`INPZOLWWEn6>|ZyL;YCd~og4kvk73mS=EmwJ|N9QJ(Q) zi^yW5|hRvaf%)>Q%S9hi&_1GfVFp^8{{((Mcgg9!LFilEhY3H{ue~$#T4j59>pLdCb7=uv zm8+h~)@kl#k_ub5Hul~_&u!0{^_Fdwp8JXK?Kds~As%f`+fRJ?S-}q7*H6#S>Mwus zh55kQMMdjOXP+;34rmC~U8Q@r&r$H&*?g-@9(7Dy54Y}n?RLJ~IDqHB=2B7JGmef; z>(=u6@;e4zTiPL3(cZa@|CP};(*xEu5lxp^@K& ztl5=5NT(Zp=bO!V;ouFy9py7*m^9aPKl;YE*;}D4_jSazdk^o+yYVlUcC2m>cUI^O zbvtt}F^edT!H&zVSsnNErw3x%coRze3+q-8*NWf3}-&TuuL0oh)MnTXLz-V@oE###48fdG`{Fn72IYx@_H}-x!s3=nLQa zXAb(VfmQ7nIR)k@sC&HsP{N_|`1#QnvTjulCbN4)_8xXum>n9^b}x~cBfU)1@x1~2 z662l|=dFA48_Hfi`pkD;-Jzyw%a=We-4*6PUHWFvVI_kD3QCsmFGMo4Ut!t)GpDby zl|^&sPWIe{rlq=D#kL(_bzXQij#2(fs@lBL#))gzt6qB}X0cG)@O=T-o(a>Ym)~GW zGqRpKt^7q!gLKxVPkg@*_}K)9-Aj~{SfsD@-oT8x?}XPW>ov^YkuFc4$j*}c(4wvW z{sQZ=O`cvSt@nIs(2ffF#5emu*tI~(%Jz$3P^%=kx59CVQNx->(o% ztY~L`ARa7n@8O(>cTOL&w&9lWa}~R{kTZYL$5)<{9+$WVDjzNUwdjRs<6|C|i?@xA zSBknOhxQpyYc0R=z`{7PqFuLd^Muy&6GhiHec-z-b9{Ph`447`XFekL68oRVbRDvm z*&q`va8L1ey~iQzC%@vm4_QC?c{$~w?596>9L4S>&VBm4Z_@`pZJqblCk|QbY})f) zgZsU~@lE9)mL=V&*_VFP`q%lU@)MurV*|fTdulJ|dNCsD^vB@(k5tGl|r(yly0 z;yYh)&AS~I?_|?{++(b~V}0iRUS+18l+-AlZ2dV%Hi>c0GE z+MMqVder(O_fD-*c__O{d;X66=iZCTBNn9|dhx%Iq4Qqi#A#NGzxM{bke!yfk(n*H z-a4LX@8KX_=6{<^@1L7)_kKf!ci(=V`)8-yyx$OW`s19vPYylPo^tlNQ1x+MmnFfE zOHSUs-J2TB*S}z%{K@6Vj&^OG;*oRtAN#>aAx@@?vd^8HRbwB2XwTtecWYzj?w`Nj zWzXRYF;z7`)82cpD34h6bmrRG%N9Y{M?cQ=QeM(h)qXT8%BOZ}_xGh&taYvl&HlJV zx@}6p`wa=)-p4O|JDldZqC8^L)7FK>$IecO6nZ5qmiu$xLw9qo*{Z(x5`X8}pVf0p z^ITpYvFqs!Y1W^1Inz?A+Mh$o+~{kjJ(4jFG| zEzK~SfB2-)BPs5+>f8Nua?EcBWuHlU#LZT@(NwIsB49#P)9bgK-15dkvAm0q9$>e8 zxOvGX>y{)-$;toCF8}F^y!2XDX~T>w^*y_jr?FJG3+P1N`n)R}#FWtCy%cI(eOyQL z8=qr@9Or+om9d+b+^}Xz?EG@#Uhb(kv!svSQ{?IX>hz9)-FjKpNn+2SwDB%Sl)3o>7uscj@{4QHk5Z9OUmy!+kdxM`G)n8SEZ_MU98+87$yWJL%EiqkK$aM3jgzPyg zZY=xuZ&A2)@7Nl<%JkDIt+yl2lyu(C@G}l*n3m;mbLFLT?a$mcmUEn+wCwfoe;e#f z%pdJ_4w<%^F>sdS%jDw4_pDn|JFjT|T9NrGxmfwWwM**4my4~gu|IcPQ_iuC)pWtL z!**86zxFz3h;4Q~>FygOKUeVYJw+?;I>%#^pSizYY@Bq!x+T5StX9bATup@=PenV2 z?ll32#XD2#z&r)r%B%H^FJtO!Kyn7Uf#w0fA`knY>0o>>u=1#!_V3eaPk*GRx$H0R zxGJQ%{^}I&{JJc+FV*b@x}DFwcHTGLeRBRRLFU=#J9Ia03JO~EKSy$p=$5BD41(Qs zvuC%peL1`RwC2O5c~RHfrhSo>{F3x))yJb3Cbvz@&=c5y=f!hB-f7+C?nd`N7rfJ- zvj1(en@>f%>4u0_#%;?deqOe9e&vV9vQFD$!qqzBvdS)3&tiAKr&!e;`9@E0&oznN zJNNGGp27HjgJ1mm(08&Q7M2*~Jr%Bwsh+pza8!iR*@w>rs=M;cc3t24{FL>Yj;9*) zR@(GVtFBsO@z#|4-16G>w&C{@TXh1LUwGp?efERo^#<3v6L%No%cZ@&`H3$(ZDZb} zGONk;E3TYsy)$=m$t3pJ^nH5{r)}EsK-Mf);xnK0$wMvWH;Nvfu~xaI^k(_%f2(E8 z_8tz~_~DJLSxal~`wfyhJ=N{L35oZt*X(m;xR-cU*XRSEc5dQr>oxxu{(QQ>{73zX zy@zj1omuj(_-N7Im)mZ?Jud(M!wtTNf6qVC*cW!yJp04L(#)M_qn6It_BcF!(I>wA zcdzZ;xaGgOdB5G-mH)lGMn8jK;w*U#qp{SW!>In)iblFq-oe3;*y#0cFEMADopP`th*F_ zc;U%65!y?yy?^l_T$c6mv%N=mm%rGO*3fvcZ);H2-y(~1CiN#bzLkxddQoh`1ddqk zt=IBfp7}Mq%}IX$;oVVY8=KZ%@3mhyGYT$l*I0jVVe8vGh4;6A7iw;+vu6?JmPrKcyoe$LX+>-w|D_5FuD4vCX{C8sC7F1K;4TpeqCa$fliS!TayoL`Tn zUSUv|Ha~gu+2_DIu8hL`snX$h4%e*L47b%h7jU0>UP#K{Lq;xj>kYlno?|p$FQ|G= zU&r-8@B6x>&Ius%Lx*8&FS?uqx#I&iYN-?*-C zVi~}6-`C)wY~A}NtsN(>>znkkcWuxpdLSzoYPNt+G*l|S(dvuur@hYEOj?3ZuDrM| zxuPXp>7KPt#R9Q7uhx0J4u)}yTB_ThpI;#J+T@>c2aPH^V|(;=?(&qnfHhM zzQWZoFXwd1RVilAoZmA~9K3cVXKjN@Q}v=fhi@>PNQ(27OYwdGn=#<%v1KQ%-!P^A z(4SemK68r^^QnlVnxFV$7xV@GOX<&^Rr9IA;@R~t744=96V5J}XLa}RtYh7CEd@@f zc=eV?uxNd3>)ri%e#HLWzfLo)NU8AZE}y~K5cKC`@##70lhEedX>!DUYS% z3y)ZDx#nPeFUjb|{&OqhOjZa!l8x(ee8OUBwZ~b!<=m-dhpf*u-H;S|C_5{Ag0-p8 zp2L$3IqVvp8_RDnUEKU*)xPo>j1Mn|*M0u^o_~^^=+SH*$3Gt@XKiPYI^glrZTbRc z5z%!Q*>q35^16GA@daaS>fXaABuo@ z?+sX^4)TlcJL;O?s99}o?NcokUHQSgj2+zAx$Zxek^W(aZm*VmztzBBr0y(#+ z3WpxqEck+((=@B`*pj7f<`|k3Lp9`!_HdMDuUYN(Tp}Jl7@5x&(?_~QN>kA61+b=pa z2OWJYo95Vj>G)gOT~<@h3cfn~ob!bM-?O^AS%I%QvY$9~8#%-*UhDqR@8h)@O>z?t z-QMglIq2YPSvAMWmyW%beO98$yysqG?DS0sUd#Twurxs>M3Tl&RN?GyS~7;vnb9$6m`XxkM_j3oZ=2wxLu@cFw)TsO6Uqzm!cY z(NdmsFEMVp(ZQFpb}vHGR_t;15VpN(!!6O8tLqnh<}=kfvBz2W zbKiu@_NZy`|Ek;fulc}N|MR`DK0+vKvpPM@Z06mdhsV6hH~ar zy8bTT1P-!I=B zE3ljW|9;hZzrS33!TrPj|NR7in}74||Ndh5|NrI1*Zcph_y4{|{{O!z^8f#xo&RW; zgZ=*x$&LQ=KD01QsGPv{SwZxfgZ7z*@R6w{#+`pB@<_4AZzRo|iq9$kk$A{nhLd$*}xShIkO8p(x3Hu)G3wL@c zxcs=3K>6SMOWx^)OSP)@Yo6F*mLDA-rrUUEUr*8M?S~(~;_>{vYhTHnv%Pv3*#fR@ z{c)!FB&++mSASN{Njz|-_WA|??N5!iZ*iPr8e6-Q%lX@nnxa>Kr_|s5C45fd!N294 zERO^jCPr{3-`!_mb~7tg?)`$KV>j1%9#K?j`*R}l^RE475l8fwyj%F?l>XG8E2r+& z43Evezux2B7ELkR+h1;deYK)VVEWbWF5ZtzmMb0K6E6Ps&XQ|?tEGIGZv#1$)zBqb zqV>z%lUD)*mTX)$J0vw%HR87EcKz~~*4=AaHeD?}-#P1tb@C z?_qMl>M94VV@~hY|CP(d>j!J<+U|J4czXJJrv{(sw1|(tvv(|N())O&ELn_eLf-jP z2CZKhnKcV{?^tmA#q!2Qe=X|0u9u(LlD>#>(pk2<^$Nmo=1iNlaBcS2%W;q9EAcE` zJLlla=zDhar)0iSWqbK{_BBZ_o`~C~$N#UH#+az~?mVAV-k#$PH}~e&M(mguTYKSz zbrie($EjPUCg)sNc?5}tV>@fuHm&}2Ie6};p0?TR4IhA$i=KwLkK@z-FW-NulRB_T zUnF0vUYRu5p zj$7rXXlU*_O+w1Bz}viiQ@B`*;Xeb19r=5*xJ4b<*3Zt!F$~z@z9pll$@QDTJGYZl zzH4uMCDJAI{qg^VtpP7pLh~mClr~OSX1Y7$+0|C$YQ zeSCa4cz;X{`t2m3RDb41MPjEBcYgE-1HpG?|70^`j5IdB;{PI>U@sWFckhLXrQTKY zR^eKf@1}LMFuxLO@Ns0Xy2O6yLI?AbqM(Q3<=d^@ZRbooJzHU|%k=BVI!>r8O4zct zYNbTWk?SuDK40JX@Y>YF+6G$``#(0ZJ=wjr)2zbjryh@0n)O4YX&zTDU#vRdXvm`U znt49Ala%1e>8Z=(O#VC0Fk7ak#=`c;Yh~?14ng~8IW`7IX0mQy`$K}`yLCO^mUWUM z5ntJV@okXrSb6W>3y)n(_wvuxo+|U*+r^3TE4PE1Li65C=7$+BjF&7m3%YmTp1XM7 zIq%f76T%jGzkckJA{1y46}C5|$0;fM%Z|#|H$J@fdN_4Pl)&*H2bqgvZylL!qgXSY zP1acU!%T0LtjrgC6BK4N36?gVZ(YdC@#OTT%j>1;7u+|_Qt#0{^dV%wyx-zjje3W` z)9yag8m+jF^J&kx8JsRA_>ASkQU23MnH^3Uy__oYiLEioZ-J7v>&Mu+=W{>Zc%t#i z^mC!tDW=n!pLxW)i;wSd-m76dol7gAc_o+GYNgM=Ur|$ zu7&P7q?E{B=J;-xWY5MTm11^NFKONP-mbj)tn>8t2Ayu2_OwM)@3EL8Psw;=CjM# zeSSAduKaX%^8V)3SItYU+2;Izv6lJ3syMIRNeix=Jlt@{l;`K6W{KUmcrEWwx^N(* z=9uxDNz3wmo=r%V>N&qF<^Qo3l~+v4D|M${JAGZSb=9>4o}c9GM3fTm>#pr*j1=5^ z`K-q4Gcnty+$ghIytdS|aMzs!9N}g~`MajgW2y~I+J90{@%0S%hn3M!{!M#v<{jgj zXy@Shx$;UgxlLscPRox`fabjeg*RP))$f~SFE5@vwrnukyACNX7hRo9LqYfnfsU1qKF&ER&g9YKHy6LiZ<&%l?14dp4W$L#r3_cF&om zyLQ3X%475I6(oyZ`W$}BD(&d9qjE2wO(}h*7wwf}Ef;ug*Q_0P^AcFJv$yRvdQ-1v z-IRAcqsd~?n&T^OZ>ZBeAfM61X}~<|v-s;|X2-wXE)w?NJtw#GUv4|5EAq%@(}MY8dk3?8nuG@0gKvlX(leW($5U5;0vWGR=$imD0O!kKAQ$@8o%vsQN$A zcV=Dkv;B9UtvxR5pX;?J=!A{gu}fh zf(q_63=E9#PrmDawfOTL>xVCAu%4I_oV%hcb}l@~R&(-1-%9z}7!cp|>h|LKHrIJW zZJbVZYfb%g_3KhIIj66YA6HZa9GD|xb?=wlLSDb4{R=k=JPT2cYO4V!jbC$vCcWxh zjAEovZ=X}Yo{3|~)SqI{%AJp?+k`8hYj&J@JNgM*gIyov!403*GEcg6V9NHJ1&fxl zTi!RcyMEPRl2VOM=;@o@GlKU%l{HSfoch3ayO(PE!{5p^M?P0LOv^ke_ut5J!TUEN z3!07#`Anz`%95rVQ)ubqBi>Z8&zpIh_p9vPNk zPVoqTK}H-OS{kJtgbfG*p0 z!do)lwRc3fY%4j{tuBmYB@{PVfi=WNJ z&03Rn(?Z=bqIpr4Rsnm_%1*0>j<@1R%gXKdwfa3bT3B4TW5x3Lcb~%^_buPHG)^-` zPC7X=tibfrZRcs9Cn)WRW8KPhfPw8w>CA)sj>S1m|J%wfZJFifx^vF8^f#-Pefbk~ z!lo8lBnrQ+4dzr*KXj@-a+Z(pF3Znn3$6xNak6>enb-Sy@n_2~TU?aIwy>FRdg$tz zd-1s0)(DTJdzaPEa5aX0#&m+oFT2z2Pc6hhmp`?+BW1?+(d_N9q7MECn@n`b7D9`PrLtmWUw{r2B)2WV%U@pACuP^C85DbbeSfL zOKIDtOS(S2aPs`0Kei>$dQT=_IDAEM<&BMB%#>tKwX6;}Kl|O%`TWtJ6~|$j(I<0e)bdFi0rtuj%?nZ zIM$Eso(oJwBY!nLvr9kdIqmyj@yW6W&6Bq{O4`3ofz>zeOXHGfPgs1q=eJl#U)-~D z=Oem@qODS0rxrEH-uS)o*;%Or%=eD$bd0_2%_y{@BEW`&=~U419-g~?I(o|7p=%o^ zKlvt_HB%T~bbkIn>9hFjqz6tbE9I(Avx}8~;w>@W;${+4$lSWyW!JASotv3&2t;Uu zOMLExysny~x%E;4@O<$5Maa8iEpc<{uCCI5@6pT8i6=IMSG24<>t*<#!G?27)r-&5 z=WV-Uw?%A4_ujPfJ8z||7d{eus5{qa5v0;R|HpFDv!!15&p(3Hempad;xy8Jsr&|_%7QK1rkIWfx4*we+e`JqQquuh5C41tZ>G^Nk z{NeA!eSx`i8#|miEH>|-kUit>UMsWwzG8)kFI3#ld`MJfDmg!GZp9Vppam*l>Xt4t z@Oil~%<1|x-HEGrJBpNG3;ubV4j!HRr#Y7AtzfUXjKjmji&o83O!d09u~&Y)AXbF57<`N1eNvq)-jLg+OwM;6(Ar`wCTP2aPwzJ9)nyYE)(d-@Bgf6W1T$+Q8eG=RYsIZPo0{AH5Zg_if=)pT1ppqg$Jef>i@! z;Z(cQ#sJ2yNnR{h?hBSz*XZ=PRn%A}DYN8pMV<6{x_#--^}@(0!^iUqi+jdC?ae#? ze$gtooXS0W0c-f#`rT)v`>f{Onp&}RLPc)x>C^pNF71svtMx^ZYhsF)I7e0U(G0GQ zn-<3|^UEpE`|V;i(HErt=5%g-J<*d^lNz>rC1%dqxAJM&oKr2UBQn?Qo`3i0E6pWu z$~u(Q|E`%l|F7tR3Q_M9pRYw&%<7sVDkrHucg~a5De`KqC9ltQ?|NRkb`Jl8lb0FV zm>=(-&GpFK|L>U@(UR7xtQ(gm87DF=EIXo^G-{rd-8<#&_b@}On z`JUpp#k1SH%*!_z@;vlzUZ7&KR_oAICDTB8?t^cY&+RRE6l1fzVtch$gIccqY~%aW z&h1tV?tgWg(IGO`F0twB_S31;RriBRZ!f;$eMzZR~!^xF+jP;gxS!Rv*>b z{Z&a~UZ&%FN2LY!*_@Layt!A(?TQiwwQ2XZ?>=0Ly3WmNH;TEkGWzGA2u7}3Z`b?mmOt6| zVD=8Zspb{TA7;Oqljx*;u6ORU?-y3SOn&|Mit)1#=O?}@`_()x`0ix8bAR@y9_pHO zdvRZG)`kg3_b&6xf!0*b*&kdM&Cadww>RJP)N)nU{2QD97D_~|&0(3Z`1qj=xNzkz zm|kvt(|emo8&9H>*0qC4Oc#FM_;&M_n=I=OyNh?uWwHe>-q z_KEH}?`zYy{Z?`u&o1@=%|kc*es^ApRh+x3;p@XQd)YWSeYdv1biKMrRq*F?r6^y% zzcEFgW$>OimS|6V^fw0INpASvvr}Q(OOi|k%mNVkFox5c4LjRC8(??K=bKGxs|IW^3PK#_ntE3pX+_gUQ749%z z(YBE%L$qmkHOH}Dfl!nHlfBQP@LD$To#Q&A!nFQbUsqP2-2SMkL^#Ml$0X&z*Y!Hl z%HgJN8h(3j7lz4g_`c!iN{z|ue;eKiS;2d z6@ua_JF>j&)2|EMy&-hymHUR(k#A()-r4{5gzwcQTi=???P#0H)$t+8^8&jw1M}ZQ zyCYIk+jhvz3^YrBBf9&=vk0D|eU}zbOj!5yZrI7m z?fjQ{ewBdoc)X-IbLI4XRqH>9mQI@B82s>+;IZ#}PIK&?`?D9^KL0=W{r}s6mo^J2 zd_GuM=^^jidxwEVHMd<|)Ix^SW1-C3iJOex)tArO4(@76RF;T3?Yk1UD|haN8!IgL z)St9!=y>I6c5X+9)PL>bJlTVc4F4Dy7*rk1cE6r)n96?96k5attk(aVd;Y8F9OgX% zQ@+h-P@b&k9FoJe>!gQ5^Y)ts?;JB^CKfP8mu-^Fy?c#4mWzRb{lM1D>SKaZeQ9%7 z{uMi5e=+B@;bxwn`Um>-e*F>n%HDNBG&$96x6hm#@9ut^q(1-I`;G-qM9t`cd6q6BAZPx4+_^@pC?_TJ-%daX#hD8|Dk{ zwpe?Bfg$;pf^MHe)7lC9=FTo=P)O}tp{RCO=I2AFNv~gDQai(+;Fz$!H(H(Lg>K_6 z&CjA+|77&B`^rB*w($T1qdHTWUc>qr&w}-$jxu}CK-%;<?idr-2#_3_^VST3Kgr{j|E^gjc@*QJv=FD?+FL+xEWCPjg$0 z($)!=AFvdqp2+HL43E@g5h(kuCwoHYUj^Ph_#QOP@IMDq8-KYI+z)uc5;6!0Z_{%m=*gZ7nkH&PqDp zV`E(T`hh~^^|A|{X2v@8vC|p8Jy@asl6y*uZcJ{M_xUxCFI{|~lr<@<^uoOmLzh~1 z-Y4}}IAf}pbS+$8uV15i=5F$X6ZuE4=-1TDk^N;J^gi(8Kb|M1OwPSel1g>^9|~<= z&Ehs=Lh_2OXY3dssOCrQlsvlCF#VXE4xfp5aBPar;Nnb znH^Pqey#9j#s@)DPgAQG`C1Z-?3&v?*}r00XM4#t;QIdQ`$V4QZTyg8{^*tXK080Y zztS6zig>)6r0uX&>0HQ3#tgZb4KsFbvC&BI(1&&z$} z`!XM_$oFFW2jg_+IUncDx;}F?quYUbmQUZMmzbXZ*u5xfy8M34Gg+lmE|-M=;kkL> zxlOv-)@NA<0yn7lRaVcu%Ukp(>BY3A$KRhSmYSPta#!>}vsr=VdC8M&D`#yG$>DrF z4^%S7?3dW3y0Y;1r#Z)#NBWw()*m=Khp+fpQFdI~8dhnhis#dJ&b@G5|CnB9R!rTW zrE5c8Yc9DfqEIgK+@g9?t6WS*)5;nH2G~kX3=9lx3_^^| z42%p6U@XPR3Z}yt7#O6X>^KGn1`VhhCI$uuVJ3w7%yt&A`ZQ3X#lXPe0W}9kvoSDC zU}s=qU|=vXGBjYE2w^iaF*7hMfUpe=4Gb6;KurAqpMimC0mMWGCI$v(BsRzb0M%Yt ASO5S3 literal 0 HcmV?d00001 From c15b765a7dab223c608b558703e177fbceaa821a Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Wed, 1 Mar 2023 09:22:39 +0200 Subject: [PATCH 051/891] feat: add empty card --- templates/nostrmarket/index.html | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/templates/nostrmarket/index.html b/templates/nostrmarket/index.html index b4378d8..1235bf2 100644 --- a/templates/nostrmarket/index.html +++ b/templates/nostrmarket/index.html @@ -87,6 +87,14 @@
+ + +
+
+
+
+
+
From 131f778dc8087ef67cf58312644521ec9f39860e Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Wed, 1 Mar 2023 09:22:39 +0200 Subject: [PATCH 052/891] feat: add empty card --- templates/nostrmarket/index.html | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/templates/nostrmarket/index.html b/templates/nostrmarket/index.html index b4378d8..1235bf2 100644 --- a/templates/nostrmarket/index.html +++ b/templates/nostrmarket/index.html @@ -87,6 +87,14 @@
+ + +
+
+
+
+
+
From e9b7494bb61ac397f3b0f6bb4dfe258c23ce3ad1 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Wed, 1 Mar 2023 11:35:04 +0200 Subject: [PATCH 053/891] feat: basic `stall flow` --- crud.py | 63 +++++++- migrations.py | 6 +- models.py | 27 ++++ static/components/stall-list/stall-list.html | 157 +++++++++++++++++++ static/components/stall-list/stall-list.js | 84 ++++++++++ static/js/index.js | 3 +- templates/nostrmarket/index.html | 20 +-- views_api.py | 86 +++++++++- 8 files changed, 432 insertions(+), 14 deletions(-) create mode 100644 static/components/stall-list/stall-list.html create mode 100644 static/components/stall-list/stall-list.js diff --git a/crud.py b/crud.py index 96c5bf0..e66f639 100644 --- a/crud.py +++ b/crud.py @@ -4,7 +4,7 @@ from typing import List, Optional from lnbits.helpers import urlsafe_short_hash from . import db -from .models import Merchant, PartialMerchant, PartialZone, Zone +from .models import Merchant, PartialMerchant, PartialStall, PartialZone, Stall, Zone ######################################## MERCHANT ######################################## @@ -105,3 +105,64 @@ async def get_zones(user_id: str) -> List[Zone]: async def delete_zone(zone_id: str) -> None: await db.execute("DELETE FROM nostrmarket.zones WHERE id = ?", (zone_id,)) + + +######################################## STALL ######################################## + + +async def create_stall(user_id: str, data: PartialStall) -> Stall: + stall_id = urlsafe_short_hash() + await db.execute( + f""" + INSERT INTO nostrmarket.stalls (user_id, id, wallet, name, currency, zones, meta) + VALUES (?, ?, ?, ?, ?, ?, ?) + """, + ( + user_id, + stall_id, + data.wallet, + data.name, + data.currency, + json.dumps(data.shipping_zones), + json.dumps(dict(data.config)), + ), + ) + + stall = await get_stall(user_id, stall_id) + assert stall, "Newly created stall couldn't be retrieved" + return stall + + +async def get_stall(user_id: str, stall_id: str) -> Optional[Stall]: + row = await db.fetchone( + "SELECT * FROM nostrmarket.stalls WHERE user_id = ? AND id = ?", + ( + user_id, + stall_id, + ), + ) + return Stall.from_row(row) if row else None + + +async def get_stalls(user_id: str) -> List[Stall]: + rows = await db.fetchone( + "SELECT * FROM nostrmarket.stalls WHERE user_id = ?", + (user_id,), + ) + return [Stall.from_row(row) for row in rows] + + +async def update_stall(user_id: str, stall_id: str, **kwargs) -> Optional[Stall]: + q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) + await db.execute( + f"UPDATE market.stalls SET {q} WHERE user_id = ? AND id = ?", + (*kwargs.values(), user_id, stall_id), + ) + row = await db.fetchone( + "SELECT * FROM market.stalls WHERE user_id =? AND id = ?", + ( + user_id, + stall_id, + ), + ) + return Stall.from_row(row) if row else None diff --git a/migrations.py b/migrations.py index 04a4fe1..6d20f9b 100644 --- a/migrations.py +++ b/migrations.py @@ -18,15 +18,17 @@ async def m001_initial(db): """ Initial stalls table. """ + # user_id, id, wallet, name, currency, zones, meta await db.execute( """ CREATE TABLE nostrmarket.stalls ( + user_id TEXT NOT NULL, id TEXT PRIMARY KEY, wallet TEXT NOT NULL, name TEXT NOT NULL, currency TEXT, - shipping_zones TEXT NOT NULL, - rating REAL DEFAULT 0 + zones TEXT NOT NULL DEFAULT '[]', + meta TEXT NOT NULL DEFAULT '{}' ); """ ) diff --git a/models.py b/models.py index 40bb57f..8f98f24 100644 --- a/models.py +++ b/models.py @@ -43,3 +43,30 @@ class Zone(PartialZone): zone = cls(**dict(row)) zone.countries = json.loads(row["regions"]) return zone + + +######################################## STALLS ######################################## + + +class StallConfig(BaseModel): + image_url: Optional[str] + fiat_base_multiplier: int = 1 # todo: reminder wht is this for? + + +class PartialStall(BaseModel): + wallet: str + name: str + currency: str = "sat" + shipping_zones: List[str] = [] + config: StallConfig = StallConfig() + + +class Stall(PartialStall): + id: str + + @classmethod + def from_row(cls, row: Row) -> "Stall": + stall = cls(**dict(row)) + stall.config = StallConfig(**json.loads(row["meta"])) + stall.shipping_zones = json.loads(row["zones"]) + return stall diff --git a/static/components/stall-list/stall-list.html b/static/components/stall-list/stall-list.html new file mode 100644 index 0000000..6778614 --- /dev/null +++ b/static/components/stall-list/stall-list.html @@ -0,0 +1,157 @@ +
+
+
+ New Stall + + + +
+ +
+ +
+ + + + + + + + + +
+ Create Stall + Cancel +
+
+
+
+
+
diff --git a/static/components/stall-list/stall-list.js b/static/components/stall-list/stall-list.js new file mode 100644 index 0000000..6ba13dd --- /dev/null +++ b/static/components/stall-list/stall-list.js @@ -0,0 +1,84 @@ +async function stallList(path) { + const template = await loadTemplateAsync(path) + Vue.component('stall-list', { + name: 'stall-list', + template, + + props: [`adminkey`, 'inkey', 'wallet-options'], + data: function () { + return { + filter: '', + stalls: [], + currencies: [], + stallDialog: { + show: false, + data: { + name: '', + wallet: null, + currency: 'sat', + shippingZones: [] + } + }, + zoneOptions: [] + } + }, + methods: { + sendStallFormData: async function () { + console.log('### sendStallFormData', this.stallDialog.data) + + await this.createStall({ + name: this.stallDialog.data.name, + wallet: this.stallDialog.data.wallet, + currency: this.stallDialog.data.currency, + shipping_zones: this.stallDialog.data.shippingZones.map(z => z.id), + config: {} + }) + }, + createStall: async function (stall) { + console.log('### createStall', stall) + try { + const {data} = await LNbits.api.request( + 'POST', + '/nostrmarket/api/v1/stall', + this.adminkey, + stall + ) + } catch (error) { + LNbits.utils.notifyApiError(error) + } + }, + getCurrencies: async function () { + try { + const {data} = await LNbits.api.request( + 'GET', + '/nostrmarket/api/v1/currencies', + this.inkey + ) + + this.currencies = ['sat', ...data] + } catch (error) { + LNbits.utils.notifyApiError(error) + } + }, + getZones: async function () { + try { + const {data} = await LNbits.api.request( + 'GET', + '/nostrmarket/api/v1/zone', + this.inkey + ) + this.zoneOptions = data.map(z => ({ + id: z.id, + label: `${z.name} (${z.countries.join(', ')})` + })) + } catch (error) { + LNbits.utils.notifyApiError(error) + } + } + }, + created: async function () { + await this.getCurrencies() + await this.getZones() + } + }) +} diff --git a/static/js/index.js b/static/js/index.js index 68ece51..343b5eb 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -1,9 +1,10 @@ const merchant = async () => { Vue.component(VueQrcode.name, VueQrcode) - await stallDetails('static/components/stall-details/stall-details.html') await keyPair('static/components/key-pair/key-pair.html') await shippingZones('static/components/shipping-zones/shipping-zones.html') + await stallDetails('static/components/stall-details/stall-details.html') + await stallList('static/components/stall-list/stall-list.html') const nostr = window.NostrTools diff --git a/templates/nostrmarket/index.html b/templates/nostrmarket/index.html index 1235bf2..8e61d74 100644 --- a/templates/nostrmarket/index.html +++ b/templates/nostrmarket/index.html @@ -86,15 +86,16 @@ > + + + + +
- - -
-
-
-
-
-
@@ -115,9 +116,10 @@ - + + {% endblock %} diff --git a/views_api.py b/views_api.py index f294de0..2488b7c 100644 --- a/views_api.py +++ b/views_api.py @@ -16,14 +16,18 @@ from lnbits.utils.exchange_rates import currencies from . import nostrmarket_ext from .crud import ( create_merchant, + create_stall, create_zone, delete_zone, get_merchant_for_user, + get_stall, + get_stalls, get_zone, get_zones, + update_stall, update_zone, ) -from .models import Merchant, PartialMerchant, PartialZone, Zone +from .models import Merchant, PartialMerchant, PartialStall, PartialZone, Stall, Zone ######################################## MERCHANT ######################################## @@ -138,6 +142,86 @@ async def api_delete_zone(zone_id, wallet: WalletTypeInfo = Depends(require_admi ) +######################################## STALLS ######################################## + + +@nostrmarket_ext.post("/api/v1/stall") +async def api_create_stall( + data: PartialStall, + wallet: WalletTypeInfo = Depends(require_invoice_key), +): + try: + stall = await create_stall(wallet.wallet.user, data=data) + return stall.dict() + 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_invoice_key), +): + try: + stall = await get_stall(wallet.wallet.user, data.id) + if not stall: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail="Stall does not exist.", + ) + stall = await update_stall(wallet.wallet.user, data.id, **data.dict()) + assert stall, "Cannot fetch updated stall" + return stall.dict() + except HTTPException as ex: + raise ex + except Exception as ex: + logger.warning(ex) + raise HTTPException( + status_code=HTTPStatus.INTERNAL_SERVER_ERROR, + detail="Cannot create 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 create stall", + ) + + +@nostrmarket_ext.get("/api/v1/stall") +async def api_gey_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 create stall", + ) + + +######################################## OTHER ######################################## + + @nostrmarket_ext.get("/api/v1/currencies") async def api_list_currencies_available(): return list(currencies.keys()) From 8ea5fbc11399dee579fd0ed7c4bc595dfb4b81ee Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Wed, 1 Mar 2023 11:35:04 +0200 Subject: [PATCH 054/891] feat: basic `stall flow` --- crud.py | 63 +++++++- migrations.py | 6 +- models.py | 27 ++++ static/components/stall-list/stall-list.html | 157 +++++++++++++++++++ static/components/stall-list/stall-list.js | 84 ++++++++++ static/js/index.js | 3 +- templates/nostrmarket/index.html | 20 +-- views_api.py | 86 +++++++++- 8 files changed, 432 insertions(+), 14 deletions(-) create mode 100644 static/components/stall-list/stall-list.html create mode 100644 static/components/stall-list/stall-list.js diff --git a/crud.py b/crud.py index 96c5bf0..e66f639 100644 --- a/crud.py +++ b/crud.py @@ -4,7 +4,7 @@ from typing import List, Optional from lnbits.helpers import urlsafe_short_hash from . import db -from .models import Merchant, PartialMerchant, PartialZone, Zone +from .models import Merchant, PartialMerchant, PartialStall, PartialZone, Stall, Zone ######################################## MERCHANT ######################################## @@ -105,3 +105,64 @@ async def get_zones(user_id: str) -> List[Zone]: async def delete_zone(zone_id: str) -> None: await db.execute("DELETE FROM nostrmarket.zones WHERE id = ?", (zone_id,)) + + +######################################## STALL ######################################## + + +async def create_stall(user_id: str, data: PartialStall) -> Stall: + stall_id = urlsafe_short_hash() + await db.execute( + f""" + INSERT INTO nostrmarket.stalls (user_id, id, wallet, name, currency, zones, meta) + VALUES (?, ?, ?, ?, ?, ?, ?) + """, + ( + user_id, + stall_id, + data.wallet, + data.name, + data.currency, + json.dumps(data.shipping_zones), + json.dumps(dict(data.config)), + ), + ) + + stall = await get_stall(user_id, stall_id) + assert stall, "Newly created stall couldn't be retrieved" + return stall + + +async def get_stall(user_id: str, stall_id: str) -> Optional[Stall]: + row = await db.fetchone( + "SELECT * FROM nostrmarket.stalls WHERE user_id = ? AND id = ?", + ( + user_id, + stall_id, + ), + ) + return Stall.from_row(row) if row else None + + +async def get_stalls(user_id: str) -> List[Stall]: + rows = await db.fetchone( + "SELECT * FROM nostrmarket.stalls WHERE user_id = ?", + (user_id,), + ) + return [Stall.from_row(row) for row in rows] + + +async def update_stall(user_id: str, stall_id: str, **kwargs) -> Optional[Stall]: + q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) + await db.execute( + f"UPDATE market.stalls SET {q} WHERE user_id = ? AND id = ?", + (*kwargs.values(), user_id, stall_id), + ) + row = await db.fetchone( + "SELECT * FROM market.stalls WHERE user_id =? AND id = ?", + ( + user_id, + stall_id, + ), + ) + return Stall.from_row(row) if row else None diff --git a/migrations.py b/migrations.py index 04a4fe1..6d20f9b 100644 --- a/migrations.py +++ b/migrations.py @@ -18,15 +18,17 @@ async def m001_initial(db): """ Initial stalls table. """ + # user_id, id, wallet, name, currency, zones, meta await db.execute( """ CREATE TABLE nostrmarket.stalls ( + user_id TEXT NOT NULL, id TEXT PRIMARY KEY, wallet TEXT NOT NULL, name TEXT NOT NULL, currency TEXT, - shipping_zones TEXT NOT NULL, - rating REAL DEFAULT 0 + zones TEXT NOT NULL DEFAULT '[]', + meta TEXT NOT NULL DEFAULT '{}' ); """ ) diff --git a/models.py b/models.py index 40bb57f..8f98f24 100644 --- a/models.py +++ b/models.py @@ -43,3 +43,30 @@ class Zone(PartialZone): zone = cls(**dict(row)) zone.countries = json.loads(row["regions"]) return zone + + +######################################## STALLS ######################################## + + +class StallConfig(BaseModel): + image_url: Optional[str] + fiat_base_multiplier: int = 1 # todo: reminder wht is this for? + + +class PartialStall(BaseModel): + wallet: str + name: str + currency: str = "sat" + shipping_zones: List[str] = [] + config: StallConfig = StallConfig() + + +class Stall(PartialStall): + id: str + + @classmethod + def from_row(cls, row: Row) -> "Stall": + stall = cls(**dict(row)) + stall.config = StallConfig(**json.loads(row["meta"])) + stall.shipping_zones = json.loads(row["zones"]) + return stall diff --git a/static/components/stall-list/stall-list.html b/static/components/stall-list/stall-list.html new file mode 100644 index 0000000..6778614 --- /dev/null +++ b/static/components/stall-list/stall-list.html @@ -0,0 +1,157 @@ +
+
+
+ New Stall + + + +
+ +
+ +
+ + + + + + + + + +
+ Create Stall + Cancel +
+
+
+
+
+
diff --git a/static/components/stall-list/stall-list.js b/static/components/stall-list/stall-list.js new file mode 100644 index 0000000..6ba13dd --- /dev/null +++ b/static/components/stall-list/stall-list.js @@ -0,0 +1,84 @@ +async function stallList(path) { + const template = await loadTemplateAsync(path) + Vue.component('stall-list', { + name: 'stall-list', + template, + + props: [`adminkey`, 'inkey', 'wallet-options'], + data: function () { + return { + filter: '', + stalls: [], + currencies: [], + stallDialog: { + show: false, + data: { + name: '', + wallet: null, + currency: 'sat', + shippingZones: [] + } + }, + zoneOptions: [] + } + }, + methods: { + sendStallFormData: async function () { + console.log('### sendStallFormData', this.stallDialog.data) + + await this.createStall({ + name: this.stallDialog.data.name, + wallet: this.stallDialog.data.wallet, + currency: this.stallDialog.data.currency, + shipping_zones: this.stallDialog.data.shippingZones.map(z => z.id), + config: {} + }) + }, + createStall: async function (stall) { + console.log('### createStall', stall) + try { + const {data} = await LNbits.api.request( + 'POST', + '/nostrmarket/api/v1/stall', + this.adminkey, + stall + ) + } catch (error) { + LNbits.utils.notifyApiError(error) + } + }, + getCurrencies: async function () { + try { + const {data} = await LNbits.api.request( + 'GET', + '/nostrmarket/api/v1/currencies', + this.inkey + ) + + this.currencies = ['sat', ...data] + } catch (error) { + LNbits.utils.notifyApiError(error) + } + }, + getZones: async function () { + try { + const {data} = await LNbits.api.request( + 'GET', + '/nostrmarket/api/v1/zone', + this.inkey + ) + this.zoneOptions = data.map(z => ({ + id: z.id, + label: `${z.name} (${z.countries.join(', ')})` + })) + } catch (error) { + LNbits.utils.notifyApiError(error) + } + } + }, + created: async function () { + await this.getCurrencies() + await this.getZones() + } + }) +} diff --git a/static/js/index.js b/static/js/index.js index 68ece51..343b5eb 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -1,9 +1,10 @@ const merchant = async () => { Vue.component(VueQrcode.name, VueQrcode) - await stallDetails('static/components/stall-details/stall-details.html') await keyPair('static/components/key-pair/key-pair.html') await shippingZones('static/components/shipping-zones/shipping-zones.html') + await stallDetails('static/components/stall-details/stall-details.html') + await stallList('static/components/stall-list/stall-list.html') const nostr = window.NostrTools diff --git a/templates/nostrmarket/index.html b/templates/nostrmarket/index.html index 1235bf2..8e61d74 100644 --- a/templates/nostrmarket/index.html +++ b/templates/nostrmarket/index.html @@ -86,15 +86,16 @@ > + + + + +
- - -
-
-
-
-
-
@@ -115,9 +116,10 @@ - + + {% endblock %} diff --git a/views_api.py b/views_api.py index f294de0..2488b7c 100644 --- a/views_api.py +++ b/views_api.py @@ -16,14 +16,18 @@ from lnbits.utils.exchange_rates import currencies from . import nostrmarket_ext from .crud import ( create_merchant, + create_stall, create_zone, delete_zone, get_merchant_for_user, + get_stall, + get_stalls, get_zone, get_zones, + update_stall, update_zone, ) -from .models import Merchant, PartialMerchant, PartialZone, Zone +from .models import Merchant, PartialMerchant, PartialStall, PartialZone, Stall, Zone ######################################## MERCHANT ######################################## @@ -138,6 +142,86 @@ async def api_delete_zone(zone_id, wallet: WalletTypeInfo = Depends(require_admi ) +######################################## STALLS ######################################## + + +@nostrmarket_ext.post("/api/v1/stall") +async def api_create_stall( + data: PartialStall, + wallet: WalletTypeInfo = Depends(require_invoice_key), +): + try: + stall = await create_stall(wallet.wallet.user, data=data) + return stall.dict() + 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_invoice_key), +): + try: + stall = await get_stall(wallet.wallet.user, data.id) + if not stall: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail="Stall does not exist.", + ) + stall = await update_stall(wallet.wallet.user, data.id, **data.dict()) + assert stall, "Cannot fetch updated stall" + return stall.dict() + except HTTPException as ex: + raise ex + except Exception as ex: + logger.warning(ex) + raise HTTPException( + status_code=HTTPStatus.INTERNAL_SERVER_ERROR, + detail="Cannot create 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 create stall", + ) + + +@nostrmarket_ext.get("/api/v1/stall") +async def api_gey_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 create stall", + ) + + +######################################## OTHER ######################################## + + @nostrmarket_ext.get("/api/v1/currencies") async def api_list_currencies_available(): return list(currencies.keys()) From aa10f639b5ecedd82daf717ade5df20b7b95f48d Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Wed, 1 Mar 2023 11:46:21 +0200 Subject: [PATCH 055/891] feat: create stall --- static/components/stall-list/stall-list.html | 8 ++++++-- static/components/stall-list/stall-list.js | 17 ++++++++++++++++- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/static/components/stall-list/stall-list.html b/static/components/stall-list/stall-list.html index 6778614..ba21bc4 100644 --- a/static/components/stall-list/stall-list.html +++ b/static/components/stall-list/stall-list.html @@ -2,7 +2,7 @@
Create Stall diff --git a/static/components/stall-list/stall-list.js b/static/components/stall-list/stall-list.js index 6ba13dd..7ecf91f 100644 --- a/static/components/stall-list/stall-list.js +++ b/static/components/stall-list/stall-list.js @@ -35,7 +35,6 @@ async function stallList(path) { }) }, createStall: async function (stall) { - console.log('### createStall', stall) try { const {data} = await LNbits.api.request( 'POST', @@ -43,6 +42,11 @@ async function stallList(path) { this.adminkey, stall ) + this.stallDialog.show = false + this.$q.notify({ + type: 'positive', + message: 'Stall created!' + }) } catch (error) { LNbits.utils.notifyApiError(error) } @@ -74,6 +78,17 @@ async function stallList(path) { } catch (error) { LNbits.utils.notifyApiError(error) } + }, + openCreateStallDialog: async function () { + await this.getCurrencies() + await this.getZones() + this.stallDialog.data = { + name: '', + wallet: null, + currency: 'sat', + shippingZones: [] + } + this.stallDialog.show = true } }, created: async function () { From 187b5bc72b1865f14908f7411e7ddb7de786c3ae Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Wed, 1 Mar 2023 11:46:21 +0200 Subject: [PATCH 056/891] feat: create stall --- static/components/stall-list/stall-list.html | 8 ++++++-- static/components/stall-list/stall-list.js | 17 ++++++++++++++++- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/static/components/stall-list/stall-list.html b/static/components/stall-list/stall-list.html index 6778614..ba21bc4 100644 --- a/static/components/stall-list/stall-list.html +++ b/static/components/stall-list/stall-list.html @@ -2,7 +2,7 @@
Create Stall diff --git a/static/components/stall-list/stall-list.js b/static/components/stall-list/stall-list.js index 6ba13dd..7ecf91f 100644 --- a/static/components/stall-list/stall-list.js +++ b/static/components/stall-list/stall-list.js @@ -35,7 +35,6 @@ async function stallList(path) { }) }, createStall: async function (stall) { - console.log('### createStall', stall) try { const {data} = await LNbits.api.request( 'POST', @@ -43,6 +42,11 @@ async function stallList(path) { this.adminkey, stall ) + this.stallDialog.show = false + this.$q.notify({ + type: 'positive', + message: 'Stall created!' + }) } catch (error) { LNbits.utils.notifyApiError(error) } @@ -74,6 +78,17 @@ async function stallList(path) { } catch (error) { LNbits.utils.notifyApiError(error) } + }, + openCreateStallDialog: async function () { + await this.getCurrencies() + await this.getZones() + this.stallDialog.data = { + name: '', + wallet: null, + currency: 'sat', + shippingZones: [] + } + this.stallDialog.show = true } }, created: async function () { From 7fad3fc38dcbb4ed166d4ffb281636c2ab031d41 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Wed, 1 Mar 2023 11:49:30 +0200 Subject: [PATCH 057/891] feat: add icon to keys --- templates/nostrmarket/index.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/templates/nostrmarket/index.html b/templates/nostrmarket/index.html index 8e61d74..bb1517f 100644 --- a/templates/nostrmarket/index.html +++ b/templates/nostrmarket/index.html @@ -71,7 +71,8 @@
Show Public or Private keys From 3575825a45ee5b7904fad4e0c9f4712c714a022a Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Wed, 1 Mar 2023 11:49:30 +0200 Subject: [PATCH 058/891] feat: add icon to keys --- templates/nostrmarket/index.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/templates/nostrmarket/index.html b/templates/nostrmarket/index.html index 8e61d74..bb1517f 100644 --- a/templates/nostrmarket/index.html +++ b/templates/nostrmarket/index.html @@ -71,7 +71,8 @@
Show Public or Private keys From 7162f44450d04a4f33a547f74e4d24b47bd1d688 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Wed, 1 Mar 2023 14:09:39 +0200 Subject: [PATCH 059/891] feat: refine table columns --- migrations.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/migrations.py b/migrations.py index 6d20f9b..ae4ac68 100644 --- a/migrations.py +++ b/migrations.py @@ -44,10 +44,9 @@ async def m001_initial(db): name TEXT NOT NULL, categories TEXT, description TEXT, - image TEXT, + image_urls TEXT NOT NULL DEFAULT '[]', price REAL NOT NULL, - quantity INTEGER NOT NULL, - rating REAL DEFAULT 0 + quantity INTEGER NOT NULL ); """ ) From 83d4d5727be2ed7ac23e7f6c6e3123ee7593abb2 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Wed, 1 Mar 2023 14:09:39 +0200 Subject: [PATCH 060/891] feat: refine table columns --- migrations.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/migrations.py b/migrations.py index 6d20f9b..ae4ac68 100644 --- a/migrations.py +++ b/migrations.py @@ -44,10 +44,9 @@ async def m001_initial(db): name TEXT NOT NULL, categories TEXT, description TEXT, - image TEXT, + image_urls TEXT NOT NULL DEFAULT '[]', price REAL NOT NULL, - quantity INTEGER NOT NULL, - rating REAL DEFAULT 0 + quantity INTEGER NOT NULL ); """ ) From 7f7f5a08b094e61a5d90f46ac81e0276a11520cc Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Wed, 1 Mar 2023 14:23:51 +0200 Subject: [PATCH 061/891] fix: column name --- migrations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/migrations.py b/migrations.py index ae4ac68..8af5726 100644 --- a/migrations.py +++ b/migrations.py @@ -44,7 +44,7 @@ async def m001_initial(db): name TEXT NOT NULL, categories TEXT, description TEXT, - image_urls TEXT NOT NULL DEFAULT '[]', + images TEXT NOT NULL DEFAULT '[]', price REAL NOT NULL, quantity INTEGER NOT NULL ); From 3542b1a08b3d076008a15fff2c4ae8cb5bc2457b Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Wed, 1 Mar 2023 14:23:51 +0200 Subject: [PATCH 062/891] fix: column name --- migrations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/migrations.py b/migrations.py index ae4ac68..8af5726 100644 --- a/migrations.py +++ b/migrations.py @@ -44,7 +44,7 @@ async def m001_initial(db): name TEXT NOT NULL, categories TEXT, description TEXT, - image_urls TEXT NOT NULL DEFAULT '[]', + images TEXT NOT NULL DEFAULT '[]', price REAL NOT NULL, quantity INTEGER NOT NULL ); From aba3706a71043b50dc3f4f0d4729cfce7c0d57a4 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Wed, 1 Mar 2023 14:57:18 +0200 Subject: [PATCH 063/891] feat: minor UI improvements --- static/components/shipping-zones/shipping-zones.html | 1 - templates/nostrmarket/index.html | 7 ++++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/static/components/shipping-zones/shipping-zones.html b/static/components/shipping-zones/shipping-zones.html index 100afe9..e013302 100644 --- a/static/components/shipping-zones/shipping-zones.html +++ b/static/components/shipping-zones/shipping-zones.html @@ -4,7 +4,6 @@ unelevated color="primary" icon="public" - label="Shipping Zones" @click="openZoneDialog()" > diff --git a/templates/nostrmarket/index.html b/templates/nostrmarket/index.html index bb1517f..6ebd25c 100644 --- a/templates/nostrmarket/index.html +++ b/templates/nostrmarket/index.html @@ -61,19 +61,20 @@
-
-
+
+
Show Public or Private keys From 7ec923ea4d71c6f5332aa7ebef484b71350b2542 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Wed, 1 Mar 2023 14:57:18 +0200 Subject: [PATCH 064/891] feat: minor UI improvements --- static/components/shipping-zones/shipping-zones.html | 1 - templates/nostrmarket/index.html | 7 ++++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/static/components/shipping-zones/shipping-zones.html b/static/components/shipping-zones/shipping-zones.html index 100afe9..e013302 100644 --- a/static/components/shipping-zones/shipping-zones.html +++ b/static/components/shipping-zones/shipping-zones.html @@ -4,7 +4,6 @@ unelevated color="primary" icon="public" - label="Shipping Zones" @click="openZoneDialog()" > diff --git a/templates/nostrmarket/index.html b/templates/nostrmarket/index.html index bb1517f..6ebd25c 100644 --- a/templates/nostrmarket/index.html +++ b/templates/nostrmarket/index.html @@ -61,19 +61,20 @@
-
-
+
+
Show Public or Private keys From a215dc465dcab40861f37d1802dad0d2cbcb400f Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Wed, 1 Mar 2023 16:59:59 +0000 Subject: [PATCH 065/891] market component (ready until final product object) --- .../customer-market/customer-market.html | 83 ++++++++ .../customer-market/customer-market.js | 17 ++ templates/nostrmarket/market.html | 180 +++++++----------- views.py | 10 +- 4 files changed, 169 insertions(+), 121 deletions(-) create mode 100644 static/components/customer-market/customer-market.html create mode 100644 static/components/customer-market/customer-market.js diff --git a/static/components/customer-market/customer-market.html b/static/components/customer-market/customer-market.html new file mode 100644 index 0000000..99af972 --- /dev/null +++ b/static/components/customer-market/customer-market.html @@ -0,0 +1,83 @@ +
+
+ + + + +
+
+ {{ item.product }} +
+
+ + +
+ + +
+
+ {{ item.stallName }} +
+ + {{ item.price }} satsBTC {{ (item.price / 1e8).toFixed(8) }} + + + {{ item.formatedPrice }} + ({{ item.priceInSats }} sats) + + {{ item.amount }} left +
+
+ {{cat}} +
+
+

{{ item.description }}

+
+
+ + + + + Stall: {{ item.stallName }} +
+ See product + + Visit Stall + +
+
+
+
+
diff --git a/static/components/customer-market/customer-market.js b/static/components/customer-market/customer-market.js new file mode 100644 index 0000000..5844105 --- /dev/null +++ b/static/components/customer-market/customer-market.js @@ -0,0 +1,17 @@ +async function customerMarket(path) { + const template = await loadTemplateAsync(path) + Vue.component('customer-market', { + name: 'customer-market', + template, + + props: ['products', 'exchange-rates'], + data: function () { + return {} + }, + methods: { + changePage() { + return + } + } + }) +} diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html index a85f2fc..dba048d 100644 --- a/templates/nostrmarket/market.html +++ b/templates/nostrmarket/market.html @@ -3,6 +3,7 @@ Settings +
@@ -83,7 +84,7 @@ label="Relay URL" hint="Add relays" > - + @@ -114,123 +115,42 @@
- -
- Market: -
-
- - - -
+ + + {%raw%} + + {{ activePage }} + + {%endraw%} + + +
-
-
- - {% 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 %} + + + + + + From 5bc6e3530897ccd2a9362028736f22200fee3294 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Thu, 2 Mar 2023 13:31:42 +0200 Subject: [PATCH 076/891] feat: add basic stall details --- .../stall-details/stall-details.html | 109 ++++++++++++++++++ .../components/stall-details/stall-details.js | 39 ++++++- static/components/stall-list/stall-list.html | 11 +- templates/nostrmarket/index.html | 3 +- 4 files changed, 158 insertions(+), 4 deletions(-) diff --git a/static/components/stall-details/stall-details.html b/static/components/stall-details/stall-details.html index e69de29..73a11d2 100644 --- a/static/components/stall-details/stall-details.html +++ b/static/components/stall-details/stall-details.html @@ -0,0 +1,109 @@ +
+ + + + + + + +
+
+
Name:
+
+ +
+
+
+
+
Description:
+
+ +
+
+
+
+
Wallet:
+
+ + +
+
+
+
+
Currency:
+
+ +
+
+
+
+
Shipping Zones:
+
+ +
+
+
+
+
+ +
+
+ +
+
+
+ +
diff --git a/static/components/stall-details/stall-details.js b/static/components/stall-details/stall-details.js index 5b929e1..ab81ef1 100644 --- a/static/components/stall-details/stall-details.js +++ b/static/components/stall-details/stall-details.js @@ -1,15 +1,50 @@ async function stallDetails(path) { const template = await loadTemplateAsync(path) + Vue.component('stall-details', { name: 'stall-details', template, - //props: ['stall-id', 'adminkey', 'inkey', 'wallet-options'], + props: [ + 'stall-id', + 'adminkey', + 'inkey', + 'wallet-options', + 'zone-options', + 'currencies' + ], data: function () { return { tab: 'info', - relay: null + stall: null + // currencies: [], } + }, + computed: { + filteredZoneOptions: function () { + if (!this.stall) return [] + return this.zoneOptions.filter(z => z.currency === this.stall.currency) + } + }, + methods: { + getStall: async function () { + try { + const {data} = await LNbits.api.request( + 'GET', + '/nostrmarket/api/v1/stall/' + this.stallId, + this.inkey + ) + this.stall = data + console.log('### this.stall', this.stall) + } catch (error) { + LNbits.utils.notifyApiError(error) + } + } + + }, + created: async function () { + await this.getStall() + console.log('### this.zoneOptions', this.zoneOptions) } }) } diff --git a/static/components/stall-list/stall-list.html b/static/components/stall-list/stall-list.html index a039c30..9ef981a 100644 --- a/static/components/stall-list/stall-list.html +++ b/static/components/stall-list/stall-list.html @@ -86,7 +86,16 @@
- + +
diff --git a/templates/nostrmarket/index.html b/templates/nostrmarket/index.html index 6ebd25c..41fa040 100644 --- a/templates/nostrmarket/index.html +++ b/templates/nostrmarket/index.html @@ -76,7 +76,7 @@ color="primary" class="float-right" > - Show Public or Private keys + Show Public and Private keys
@@ -115,6 +115,7 @@
{% endblock%}{% block scripts %} {{ window_vars(user) }} + From f95db754953023c0c43131f351459d9f585f13aa Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Thu, 2 Mar 2023 13:41:10 +0200 Subject: [PATCH 077/891] fix: store id for shipping zone --- models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/models.py b/models.py index 060b462..e52ba6e 100644 --- a/models.py +++ b/models.py @@ -66,7 +66,7 @@ class PartialStall(BaseModel): wallet: str name: str currency: str = "sat" - shipping_zones: List[PartialZone] = [] + shipping_zones: List[Zone] = [] config: StallConfig = StallConfig() def validate_stall(self): @@ -114,5 +114,5 @@ class Stall(PartialStall): def from_row(cls, row: Row) -> "Stall": stall = cls(**dict(row)) stall.config = StallConfig(**json.loads(row["meta"])) - stall.shipping_zones = [PartialZone(**z) for z in json.loads(row["zones"])] + stall.shipping_zones = [Zone(**z) for z in json.loads(row["zones"])] return stall From 5ea6fec96731b6fa73f098b39c2f5b2b136ef94c Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Thu, 2 Mar 2023 13:41:10 +0200 Subject: [PATCH 078/891] fix: store id for shipping zone --- models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/models.py b/models.py index 060b462..e52ba6e 100644 --- a/models.py +++ b/models.py @@ -66,7 +66,7 @@ class PartialStall(BaseModel): wallet: str name: str currency: str = "sat" - shipping_zones: List[PartialZone] = [] + shipping_zones: List[Zone] = [] config: StallConfig = StallConfig() def validate_stall(self): @@ -114,5 +114,5 @@ class Stall(PartialStall): def from_row(cls, row: Row) -> "Stall": stall = cls(**dict(row)) stall.config = StallConfig(**json.loads(row["meta"])) - stall.shipping_zones = [PartialZone(**z) for z in json.loads(row["zones"])] + stall.shipping_zones = [Zone(**z) for z in json.loads(row["zones"])] return stall From 6d8488d2c31f038001fbcdb30ab542a1639c0a05 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Thu, 2 Mar 2023 13:58:49 +0200 Subject: [PATCH 079/891] fix: shipping zones label --- static/components/stall-details/stall-details.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/static/components/stall-details/stall-details.js b/static/components/stall-details/stall-details.js index ab81ef1..0caf1a3 100644 --- a/static/components/stall-details/stall-details.js +++ b/static/components/stall-details/stall-details.js @@ -35,16 +35,20 @@ async function stallDetails(path) { this.inkey ) this.stall = data + this.stall.shipping_zones.forEach( + z => + (z.label = z.name + ? `${z.name} (${z.countries.join(', ')})` + : z.countries.join(', ')) + ) console.log('### this.stall', this.stall) } catch (error) { LNbits.utils.notifyApiError(error) } } - }, created: async function () { await this.getStall() - console.log('### this.zoneOptions', this.zoneOptions) } }) } From a7bd400a13b11ec6daf7ddf3077d5f734354bd32 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Thu, 2 Mar 2023 13:58:49 +0200 Subject: [PATCH 080/891] fix: shipping zones label --- static/components/stall-details/stall-details.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/static/components/stall-details/stall-details.js b/static/components/stall-details/stall-details.js index ab81ef1..0caf1a3 100644 --- a/static/components/stall-details/stall-details.js +++ b/static/components/stall-details/stall-details.js @@ -35,16 +35,20 @@ async function stallDetails(path) { this.inkey ) this.stall = data + this.stall.shipping_zones.forEach( + z => + (z.label = z.name + ? `${z.name} (${z.countries.join(', ')})` + : z.countries.join(', ')) + ) console.log('### this.stall', this.stall) } catch (error) { LNbits.utils.notifyApiError(error) } } - }, created: async function () { await this.getStall() - console.log('### this.zoneOptions', this.zoneOptions) } }) } From c1a0e721e6478873f3576fa48c00001a6147bff3 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Thu, 2 Mar 2023 14:18:48 +0200 Subject: [PATCH 081/891] feat: update and delete stall --- .../stall-details/stall-details.html | 42 ++++++------ .../components/stall-details/stall-details.js | 67 ++++++++++++++++--- 2 files changed, 80 insertions(+), 29 deletions(-) diff --git a/static/components/stall-details/stall-details.html b/static/components/stall-details/stall-details.html index 73a11d2..00b3b29 100644 --- a/static/components/stall-details/stall-details.html +++ b/static/components/stall-details/stall-details.html @@ -85,25 +85,25 @@
- +
+
+ Update Stall +
+
+ Delete Stall +
+
diff --git a/static/components/stall-details/stall-details.js b/static/components/stall-details/stall-details.js index 0caf1a3..86b09b3 100644 --- a/static/components/stall-details/stall-details.js +++ b/static/components/stall-details/stall-details.js @@ -27,6 +27,15 @@ async function stallDetails(path) { } }, methods: { + mapStall: function(stall) { + stall.shipping_zones.forEach( + z => + (z.label = z.name + ? `${z.name} (${z.countries.join(', ')})` + : z.countries.join(', ')) + ) + return stall + }, getStall: async function () { try { const {data} = await LNbits.api.request( @@ -34,18 +43,60 @@ async function stallDetails(path) { '/nostrmarket/api/v1/stall/' + this.stallId, this.inkey ) - this.stall = data - this.stall.shipping_zones.forEach( - z => - (z.label = z.name - ? `${z.name} (${z.countries.join(', ')})` - : z.countries.join(', ')) - ) + this.stall = this.mapStall(data) + console.log('### this.stall', this.stall) } catch (error) { LNbits.utils.notifyApiError(error) } - } + }, + updateRelay: async function () { + try { + const {data} = await LNbits.api.request( + 'PUT', + '/nostrmarket/api/v1/stall/' + this.stallId, + this.adminkey, + this.stall + ) + this.stall = this.mapStall(data) + this.$emit('stall-updated', this.stall) + this.$q.notify({ + type: 'positive', + message: 'Stall Updated', + timeout: 5000 + }) + } catch (error) { + console.warn(error) + LNbits.utils.notifyApiError(error) + } + }, + deleteRelay: function () { + LNbits.utils + .confirmDialog( + ` + Products and orders will be deleted also! + Are you sure you want to delete this relay? + ` + ) + .onOk(async () => { + try { + await LNbits.api.request( + 'DELETE', + '/nostrmarket/api/v1/stall/' + this.stallId, + this.adminkey + ) + this.$emit('stall-deleted', this.stallId) + this.$q.notify({ + type: 'positive', + message: 'Stall Deleted', + timeout: 5000 + }) + } catch (error) { + console.warn(error) + LNbits.utils.notifyApiError(error) + } + }) + }, }, created: async function () { await this.getStall() From fd302571ad10622731c016bc7648ac01cd6f2829 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Thu, 2 Mar 2023 14:18:48 +0200 Subject: [PATCH 082/891] feat: update and delete stall --- .../stall-details/stall-details.html | 42 ++++++------ .../components/stall-details/stall-details.js | 67 ++++++++++++++++--- 2 files changed, 80 insertions(+), 29 deletions(-) diff --git a/static/components/stall-details/stall-details.html b/static/components/stall-details/stall-details.html index 73a11d2..00b3b29 100644 --- a/static/components/stall-details/stall-details.html +++ b/static/components/stall-details/stall-details.html @@ -85,25 +85,25 @@
- +
+
+ Update Stall +
+
+ Delete Stall +
+
diff --git a/static/components/stall-details/stall-details.js b/static/components/stall-details/stall-details.js index 0caf1a3..86b09b3 100644 --- a/static/components/stall-details/stall-details.js +++ b/static/components/stall-details/stall-details.js @@ -27,6 +27,15 @@ async function stallDetails(path) { } }, methods: { + mapStall: function(stall) { + stall.shipping_zones.forEach( + z => + (z.label = z.name + ? `${z.name} (${z.countries.join(', ')})` + : z.countries.join(', ')) + ) + return stall + }, getStall: async function () { try { const {data} = await LNbits.api.request( @@ -34,18 +43,60 @@ async function stallDetails(path) { '/nostrmarket/api/v1/stall/' + this.stallId, this.inkey ) - this.stall = data - this.stall.shipping_zones.forEach( - z => - (z.label = z.name - ? `${z.name} (${z.countries.join(', ')})` - : z.countries.join(', ')) - ) + this.stall = this.mapStall(data) + console.log('### this.stall', this.stall) } catch (error) { LNbits.utils.notifyApiError(error) } - } + }, + updateRelay: async function () { + try { + const {data} = await LNbits.api.request( + 'PUT', + '/nostrmarket/api/v1/stall/' + this.stallId, + this.adminkey, + this.stall + ) + this.stall = this.mapStall(data) + this.$emit('stall-updated', this.stall) + this.$q.notify({ + type: 'positive', + message: 'Stall Updated', + timeout: 5000 + }) + } catch (error) { + console.warn(error) + LNbits.utils.notifyApiError(error) + } + }, + deleteRelay: function () { + LNbits.utils + .confirmDialog( + ` + Products and orders will be deleted also! + Are you sure you want to delete this relay? + ` + ) + .onOk(async () => { + try { + await LNbits.api.request( + 'DELETE', + '/nostrmarket/api/v1/stall/' + this.stallId, + this.adminkey + ) + this.$emit('stall-deleted', this.stallId) + this.$q.notify({ + type: 'positive', + message: 'Stall Deleted', + timeout: 5000 + }) + } catch (error) { + console.warn(error) + LNbits.utils.notifyApiError(error) + } + }) + }, }, created: async function () { await this.getStall() From 5a526f86f1cfde11fbed8a7910b6e1a1d364b117 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Thu, 2 Mar 2023 14:25:23 +0200 Subject: [PATCH 083/891] chore: label updates --- models.py | 2 +- static/components/stall-details/stall-details.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/models.py b/models.py index e52ba6e..7892c93 100644 --- a/models.py +++ b/models.py @@ -104,7 +104,7 @@ class Stall(PartialStall): created_at=round(time.time()), kind=5, tags=[["e", self.config.event_id]], - content="Stall deleted", + content=f"Stall '{self.name}' deleted", ) delete_event.id = delete_event.event_id diff --git a/static/components/stall-details/stall-details.js b/static/components/stall-details/stall-details.js index 86b09b3..7f71cf0 100644 --- a/static/components/stall-details/stall-details.js +++ b/static/components/stall-details/stall-details.js @@ -75,7 +75,7 @@ async function stallDetails(path) { .confirmDialog( ` Products and orders will be deleted also! - Are you sure you want to delete this relay? + Are you sure you want to delete this stall? ` ) .onOk(async () => { From 8fbe3d33c3d7854413f153d59b35034a25e36a73 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Thu, 2 Mar 2023 14:25:23 +0200 Subject: [PATCH 084/891] chore: label updates --- models.py | 2 +- static/components/stall-details/stall-details.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/models.py b/models.py index e52ba6e..7892c93 100644 --- a/models.py +++ b/models.py @@ -104,7 +104,7 @@ class Stall(PartialStall): created_at=round(time.time()), kind=5, tags=[["e", self.config.event_id]], - content="Stall deleted", + content=f"Stall '{self.name}' deleted", ) delete_event.id = delete_event.event_id diff --git a/static/components/stall-details/stall-details.js b/static/components/stall-details/stall-details.js index 86b09b3..7f71cf0 100644 --- a/static/components/stall-details/stall-details.js +++ b/static/components/stall-details/stall-details.js @@ -75,7 +75,7 @@ async function stallDetails(path) { .confirmDialog( ` Products and orders will be deleted also! - Are you sure you want to delete this relay? + Are you sure you want to delete this stall? ` ) .onOk(async () => { From f22e47e9ce4459fa2a5d3b0207c8a5af94579977 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Thu, 2 Mar 2023 14:37:07 +0200 Subject: [PATCH 085/891] feat: handle stall delete and update --- .../components/stall-details/stall-details.html | 4 ++-- static/components/stall-details/stall-details.js | 10 +++++----- static/components/stall-list/stall-list.html | 4 ++-- static/components/stall-list/stall-list.js | 16 +++++++++++++--- 4 files changed, 22 insertions(+), 12 deletions(-) diff --git a/static/components/stall-details/stall-details.html b/static/components/stall-details/stall-details.html index 00b3b29..e2a3655 100644 --- a/static/components/stall-details/stall-details.html +++ b/static/components/stall-details/stall-details.html @@ -91,7 +91,7 @@ unelevated color="secondary" class="float-left" - @click="updateRelay()" + @click="updateStall()" >Update Stall
@@ -101,7 +101,7 @@ color="pink" icon="cancel" class="float-right" - @click="deleteRelay()" + @click="deleteStall()" >Delete Stall
diff --git a/static/components/stall-details/stall-details.js b/static/components/stall-details/stall-details.js index 7f71cf0..98f9bfb 100644 --- a/static/components/stall-details/stall-details.js +++ b/static/components/stall-details/stall-details.js @@ -27,7 +27,7 @@ async function stallDetails(path) { } }, methods: { - mapStall: function(stall) { + mapStall: function (stall) { stall.shipping_zones.forEach( z => (z.label = z.name @@ -44,13 +44,13 @@ async function stallDetails(path) { this.inkey ) this.stall = this.mapStall(data) - + console.log('### this.stall', this.stall) } catch (error) { LNbits.utils.notifyApiError(error) } }, - updateRelay: async function () { + updateStall: async function () { try { const {data} = await LNbits.api.request( 'PUT', @@ -70,7 +70,7 @@ async function stallDetails(path) { LNbits.utils.notifyApiError(error) } }, - deleteRelay: function () { + deleteStall: function () { LNbits.utils .confirmDialog( ` @@ -96,7 +96,7 @@ async function stallDetails(path) { LNbits.utils.notifyApiError(error) } }) - }, + } }, created: async function () { await this.getStall() diff --git a/static/components/stall-list/stall-list.html b/static/components/stall-list/stall-list.html index 9ef981a..84e8fd4 100644 --- a/static/components/stall-list/stall-list.html +++ b/static/components/stall-list/stall-list.html @@ -93,9 +93,9 @@ :wallet-options="walletOptions" :zone-options="zoneOptions" :currencies="currencies" + @stall-deleted="handleStallDeleted" + @stall-updated="handleStallUpdated" > -
diff --git a/static/components/stall-list/stall-list.js b/static/components/stall-list/stall-list.js index 1ca67fe..7fb3f8c 100644 --- a/static/components/stall-list/stall-list.js +++ b/static/components/stall-list/stall-list.js @@ -88,6 +88,7 @@ async function stallList(path) { stall ) this.stallDialog.show = false + this.stalls.unshift(data) this.$q.notify({ type: 'positive', message: 'Stall created!' @@ -116,7 +117,6 @@ async function stallList(path) { '/nostrmarket/api/v1/stall', this.inkey ) - console.log('### stalls', data) this.stalls = data.map(s => ({...s, expanded: false})) } catch (error) { LNbits.utils.notifyApiError(error) @@ -129,18 +129,28 @@ async function stallList(path) { '/nostrmarket/api/v1/zone', this.inkey ) - console.log('### zones', data) this.zoneOptions = data.map(z => ({ ...z, label: z.name ? `${z.name} (${z.countries.join(', ')})` : z.countries.join(', ') })) - console.log('### this.zoneOptions', this.zoneOptions) } catch (error) { LNbits.utils.notifyApiError(error) } }, + handleStallDeleted: function (stallId) { + this.stalls = _.reject(this.stalls, function (obj) { + return obj.id === stallId + }) + }, + handleStallUpdated: function (stall) { + const index = this.stalls.findIndex(r => r.id === stall.id) + if (index !== -1) { + stall.expanded = true + this.stalls.splice(index, 1, stall) + } + }, openCreateStallDialog: async function () { await this.getCurrencies() await this.getZones() From 858449d2427711a7bec661d3426c524a9b8a2456 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Thu, 2 Mar 2023 14:37:07 +0200 Subject: [PATCH 086/891] feat: handle stall delete and update --- .../components/stall-details/stall-details.html | 4 ++-- static/components/stall-details/stall-details.js | 10 +++++----- static/components/stall-list/stall-list.html | 4 ++-- static/components/stall-list/stall-list.js | 16 +++++++++++++--- 4 files changed, 22 insertions(+), 12 deletions(-) diff --git a/static/components/stall-details/stall-details.html b/static/components/stall-details/stall-details.html index 00b3b29..e2a3655 100644 --- a/static/components/stall-details/stall-details.html +++ b/static/components/stall-details/stall-details.html @@ -91,7 +91,7 @@ unelevated color="secondary" class="float-left" - @click="updateRelay()" + @click="updateStall()" >Update Stall
@@ -101,7 +101,7 @@ color="pink" icon="cancel" class="float-right" - @click="deleteRelay()" + @click="deleteStall()" >Delete Stall
diff --git a/static/components/stall-details/stall-details.js b/static/components/stall-details/stall-details.js index 7f71cf0..98f9bfb 100644 --- a/static/components/stall-details/stall-details.js +++ b/static/components/stall-details/stall-details.js @@ -27,7 +27,7 @@ async function stallDetails(path) { } }, methods: { - mapStall: function(stall) { + mapStall: function (stall) { stall.shipping_zones.forEach( z => (z.label = z.name @@ -44,13 +44,13 @@ async function stallDetails(path) { this.inkey ) this.stall = this.mapStall(data) - + console.log('### this.stall', this.stall) } catch (error) { LNbits.utils.notifyApiError(error) } }, - updateRelay: async function () { + updateStall: async function () { try { const {data} = await LNbits.api.request( 'PUT', @@ -70,7 +70,7 @@ async function stallDetails(path) { LNbits.utils.notifyApiError(error) } }, - deleteRelay: function () { + deleteStall: function () { LNbits.utils .confirmDialog( ` @@ -96,7 +96,7 @@ async function stallDetails(path) { LNbits.utils.notifyApiError(error) } }) - }, + } }, created: async function () { await this.getStall() diff --git a/static/components/stall-list/stall-list.html b/static/components/stall-list/stall-list.html index 9ef981a..84e8fd4 100644 --- a/static/components/stall-list/stall-list.html +++ b/static/components/stall-list/stall-list.html @@ -93,9 +93,9 @@ :wallet-options="walletOptions" :zone-options="zoneOptions" :currencies="currencies" + @stall-deleted="handleStallDeleted" + @stall-updated="handleStallUpdated" > -
diff --git a/static/components/stall-list/stall-list.js b/static/components/stall-list/stall-list.js index 1ca67fe..7fb3f8c 100644 --- a/static/components/stall-list/stall-list.js +++ b/static/components/stall-list/stall-list.js @@ -88,6 +88,7 @@ async function stallList(path) { stall ) this.stallDialog.show = false + this.stalls.unshift(data) this.$q.notify({ type: 'positive', message: 'Stall created!' @@ -116,7 +117,6 @@ async function stallList(path) { '/nostrmarket/api/v1/stall', this.inkey ) - console.log('### stalls', data) this.stalls = data.map(s => ({...s, expanded: false})) } catch (error) { LNbits.utils.notifyApiError(error) @@ -129,18 +129,28 @@ async function stallList(path) { '/nostrmarket/api/v1/zone', this.inkey ) - console.log('### zones', data) this.zoneOptions = data.map(z => ({ ...z, label: z.name ? `${z.name} (${z.countries.join(', ')})` : z.countries.join(', ') })) - console.log('### this.zoneOptions', this.zoneOptions) } catch (error) { LNbits.utils.notifyApiError(error) } }, + handleStallDeleted: function (stallId) { + this.stalls = _.reject(this.stalls, function (obj) { + return obj.id === stallId + }) + }, + handleStallUpdated: function (stall) { + const index = this.stalls.findIndex(r => r.id === stall.id) + if (index !== -1) { + stall.expanded = true + this.stalls.splice(index, 1, stall) + } + }, openCreateStallDialog: async function () { await this.getCurrencies() await this.getZones() From 3b021eb5ea464ace1cf0e229421a7a8ce44438f8 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Thu, 2 Mar 2023 14:39:25 +0200 Subject: [PATCH 087/891] feat: move update & delete inside info pannel --- .../stall-details/stall-details.html | 43 ++++++++++--------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/static/components/stall-details/stall-details.html b/static/components/stall-details/stall-details.html index e2a3655..9c8eac6 100644 --- a/static/components/stall-details/stall-details.html +++ b/static/components/stall-details/stall-details.html @@ -77,6 +77,27 @@
+
+
+ Update Stall +
+
+ Delete Stall +
+
@@ -85,25 +106,5 @@
-
-
- Update Stall -
-
- Delete Stall -
-
+ From 62282d8d46fb69e382783299954c124181e81fd6 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Thu, 2 Mar 2023 14:39:25 +0200 Subject: [PATCH 088/891] feat: move update & delete inside info pannel --- .../stall-details/stall-details.html | 43 ++++++++++--------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/static/components/stall-details/stall-details.html b/static/components/stall-details/stall-details.html index e2a3655..9c8eac6 100644 --- a/static/components/stall-details/stall-details.html +++ b/static/components/stall-details/stall-details.html @@ -77,6 +77,27 @@
+
+
+ Update Stall +
+
+ Delete Stall +
+
@@ -85,25 +106,5 @@
-
-
- Update Stall -
-
- Delete Stall -
-
+ From 2acce94fe9ff076d0956a9570437780756dc526d Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Thu, 2 Mar 2023 15:52:40 +0200 Subject: [PATCH 089/891] feat: allow restore from nsec --- .../stall-details/stall-details.html | 1 - static/components/stall-list/stall-list.html | 21 ----------- static/js/index.js | 37 ++++++++++++++++--- templates/nostrmarket/index.html | 28 +++++++++++++- 4 files changed, 59 insertions(+), 28 deletions(-) diff --git a/static/components/stall-details/stall-details.html b/static/components/stall-details/stall-details.html index 9c8eac6..9f1b198 100644 --- a/static/components/stall-details/stall-details.html +++ b/static/components/stall-details/stall-details.html @@ -106,5 +106,4 @@
- diff --git a/static/components/stall-list/stall-list.html b/static/components/stall-list/stall-list.html index 84e8fd4..86ae34e 100644 --- a/static/components/stall-list/stall-list.html +++ b/static/components/stall-list/stall-list.html @@ -21,19 +21,6 @@ - - {{props.row.config.description}} diff --git a/static/js/index.js b/static/js/index.js index 343b5eb..d8de942 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -15,15 +15,41 @@ const merchant = async () => { return { merchant: {}, shippingZones: [], - showKeys: false + showKeys: false, + importKeyDialog: { + show: false, + data: { + privateKey: null + } + } } }, methods: { generateKeys: async function () { - const privkey = nostr.generatePrivateKey() - const pubkey = nostr.getPublicKey(privkey) - - const payload = {private_key: privkey, public_key: pubkey, config: {}} + const privateKey = nostr.generatePrivateKey() + await this.createMerchant(privateKey) + }, + importKeys: async function () { + this.importKeyDialog.show = false + let privateKey = this.importKeyDialog.data.privateKey + if (!privateKey) { + return + } + if (privateKey.toLowerCase().startsWith('nsec')) { + privateKey = nostr.nip19.decode(privateKey) + } + await this.createMerchant(privateKey.data) + }, + showImportKeysDialog: async function () { + this.importKeyDialog.show = true + }, + createMerchant: async function (privateKey) { + const pubkey = nostr.getPublicKey(privateKey) + const payload = { + private_key: privateKey, + public_key: pubkey, + config: {} + } try { const {data} = await LNbits.api.request( 'POST', @@ -54,6 +80,7 @@ const merchant = async () => { } }, created: async function () { + console.log('### nostr', nostr) await this.getMerchant() } }) diff --git a/templates/nostrmarket/index.html b/templates/nostrmarket/index.html index 41fa040..ad9c94d 100644 --- a/templates/nostrmarket/index.html +++ b/templates/nostrmarket/index.html @@ -38,7 +38,7 @@
+
+ + + + +
+ Import + Cancel +
+
+
+
+
{% endblock%}{% block scripts %} {{ window_vars(user) }} From 46f3ad46ad431cd3c9ed47a8432a18ca522e3447 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Thu, 2 Mar 2023 15:52:40 +0200 Subject: [PATCH 090/891] feat: allow restore from nsec --- .../stall-details/stall-details.html | 1 - static/components/stall-list/stall-list.html | 21 ----------- static/js/index.js | 37 ++++++++++++++++--- templates/nostrmarket/index.html | 28 +++++++++++++- 4 files changed, 59 insertions(+), 28 deletions(-) diff --git a/static/components/stall-details/stall-details.html b/static/components/stall-details/stall-details.html index 9c8eac6..9f1b198 100644 --- a/static/components/stall-details/stall-details.html +++ b/static/components/stall-details/stall-details.html @@ -106,5 +106,4 @@
- diff --git a/static/components/stall-list/stall-list.html b/static/components/stall-list/stall-list.html index 84e8fd4..86ae34e 100644 --- a/static/components/stall-list/stall-list.html +++ b/static/components/stall-list/stall-list.html @@ -21,19 +21,6 @@ -
- {{props.row.config.description}} diff --git a/static/js/index.js b/static/js/index.js index 343b5eb..d8de942 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -15,15 +15,41 @@ const merchant = async () => { return { merchant: {}, shippingZones: [], - showKeys: false + showKeys: false, + importKeyDialog: { + show: false, + data: { + privateKey: null + } + } } }, methods: { generateKeys: async function () { - const privkey = nostr.generatePrivateKey() - const pubkey = nostr.getPublicKey(privkey) - - const payload = {private_key: privkey, public_key: pubkey, config: {}} + const privateKey = nostr.generatePrivateKey() + await this.createMerchant(privateKey) + }, + importKeys: async function () { + this.importKeyDialog.show = false + let privateKey = this.importKeyDialog.data.privateKey + if (!privateKey) { + return + } + if (privateKey.toLowerCase().startsWith('nsec')) { + privateKey = nostr.nip19.decode(privateKey) + } + await this.createMerchant(privateKey.data) + }, + showImportKeysDialog: async function () { + this.importKeyDialog.show = true + }, + createMerchant: async function (privateKey) { + const pubkey = nostr.getPublicKey(privateKey) + const payload = { + private_key: privateKey, + public_key: pubkey, + config: {} + } try { const {data} = await LNbits.api.request( 'POST', @@ -54,6 +80,7 @@ const merchant = async () => { } }, created: async function () { + console.log('### nostr', nostr) await this.getMerchant() } }) diff --git a/templates/nostrmarket/index.html b/templates/nostrmarket/index.html index 41fa040..ad9c94d 100644 --- a/templates/nostrmarket/index.html +++ b/templates/nostrmarket/index.html @@ -38,7 +38,7 @@
+
+ + + + +
+ Import + Cancel +
+
+
+
+
{% endblock%}{% block scripts %} {{ window_vars(user) }} From 5ad070684d540dd74dafffd02d71acb7d355b7c9 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Thu, 2 Mar 2023 16:00:40 +0200 Subject: [PATCH 091/891] feat: import private key --- static/js/index.js | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/static/js/index.js b/static/js/index.js index d8de942..5b26d4c 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -35,22 +35,29 @@ const merchant = async () => { if (!privateKey) { return } - if (privateKey.toLowerCase().startsWith('nsec')) { - privateKey = nostr.nip19.decode(privateKey) + try { + if (privateKey.toLowerCase().startsWith('nsec')) { + privateKey = nostr.nip19.decode(privateKey).data + } + } catch (error) { + this.$q.notify({ + type: 'negative', + message: `${error}` + }) } - await this.createMerchant(privateKey.data) + await this.createMerchant(privateKey) }, showImportKeysDialog: async function () { this.importKeyDialog.show = true }, createMerchant: async function (privateKey) { - const pubkey = nostr.getPublicKey(privateKey) - const payload = { - private_key: privateKey, - public_key: pubkey, - config: {} - } try { + const pubkey = nostr.getPublicKey(privateKey) + const payload = { + private_key: privateKey, + public_key: pubkey, + config: {} + } const {data} = await LNbits.api.request( 'POST', '/nostrmarket/api/v1/merchant', @@ -60,10 +67,13 @@ const merchant = async () => { this.merchant = data this.$q.notify({ type: 'positive', - message: 'Keys generated!' + message: 'Merchant Created!' }) } catch (error) { - LNbits.utils.notifyApiError(error) + this.$q.notify({ + type: 'negative', + message: `${error}` + }) } }, getMerchant: async function () { @@ -80,7 +90,6 @@ const merchant = async () => { } }, created: async function () { - console.log('### nostr', nostr) await this.getMerchant() } }) From e72e315b7a82acf6bee528029f48e6c4ab547c1b Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Thu, 2 Mar 2023 16:00:40 +0200 Subject: [PATCH 092/891] feat: import private key --- static/js/index.js | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/static/js/index.js b/static/js/index.js index d8de942..5b26d4c 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -35,22 +35,29 @@ const merchant = async () => { if (!privateKey) { return } - if (privateKey.toLowerCase().startsWith('nsec')) { - privateKey = nostr.nip19.decode(privateKey) + try { + if (privateKey.toLowerCase().startsWith('nsec')) { + privateKey = nostr.nip19.decode(privateKey).data + } + } catch (error) { + this.$q.notify({ + type: 'negative', + message: `${error}` + }) } - await this.createMerchant(privateKey.data) + await this.createMerchant(privateKey) }, showImportKeysDialog: async function () { this.importKeyDialog.show = true }, createMerchant: async function (privateKey) { - const pubkey = nostr.getPublicKey(privateKey) - const payload = { - private_key: privateKey, - public_key: pubkey, - config: {} - } try { + const pubkey = nostr.getPublicKey(privateKey) + const payload = { + private_key: privateKey, + public_key: pubkey, + config: {} + } const {data} = await LNbits.api.request( 'POST', '/nostrmarket/api/v1/merchant', @@ -60,10 +67,13 @@ const merchant = async () => { this.merchant = data this.$q.notify({ type: 'positive', - message: 'Keys generated!' + message: 'Merchant Created!' }) } catch (error) { - LNbits.utils.notifyApiError(error) + this.$q.notify({ + type: 'negative', + message: `${error}` + }) } }, getMerchant: async function () { @@ -80,7 +90,6 @@ const merchant = async () => { } }, created: async function () { - console.log('### nostr', nostr) await this.getMerchant() } }) From 945f88db316c8f297d91f6605756601939de84ce Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Thu, 2 Mar 2023 15:36:11 +0000 Subject: [PATCH 093/891] add pubkey to query params --- views.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/views.py b/views.py index b9178a5..d83bc85 100644 --- a/views.py +++ b/views.py @@ -1,7 +1,7 @@ import json from http import HTTPStatus -from fastapi import Depends, Request, Query +from fastapi import Depends, Query, Request from fastapi.templating import Jinja2Templates from loguru import logger from starlette.responses import HTMLResponse @@ -24,17 +24,17 @@ async def index(request: Request, user: User = Depends(check_user_exists)): @nostrmarket_ext.get("/market", response_class=HTMLResponse) async def market( - request: Request, stall_id: str = Query(None), product_id: str = Query(None) + request: Request, + stall_id: str = Query(None), + product_id: str = Query(None), + merchant_pubkey: str = Query(None), ): return nostrmarket_renderer().TemplateResponse( "nostrmarket/market.html", - {"request": request, "stall_id": stall_id, "product_id": product_id}, - ) - - -@nostrmarket_ext.get("/stall/{stall_id}", response_class=HTMLResponse) -async def stall(request: Request, stall_id: str): - return nostrmarket_renderer().TemplateResponse( - "nostrmarket/stall.html", - {"request": request, "stall_id": stall_id}, + { + "request": request, + "stall_id": stall_id, + "product_id": product_id, + "merchant_pubkey": merchant_pubkey, + }, ) From 3db1166d5ba996d171e75560225209bf9c43a231 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Thu, 2 Mar 2023 15:36:11 +0000 Subject: [PATCH 094/891] add pubkey to query params --- views.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/views.py b/views.py index b9178a5..d83bc85 100644 --- a/views.py +++ b/views.py @@ -1,7 +1,7 @@ import json from http import HTTPStatus -from fastapi import Depends, Request, Query +from fastapi import Depends, Query, Request from fastapi.templating import Jinja2Templates from loguru import logger from starlette.responses import HTMLResponse @@ -24,17 +24,17 @@ async def index(request: Request, user: User = Depends(check_user_exists)): @nostrmarket_ext.get("/market", response_class=HTMLResponse) async def market( - request: Request, stall_id: str = Query(None), product_id: str = Query(None) + request: Request, + stall_id: str = Query(None), + product_id: str = Query(None), + merchant_pubkey: str = Query(None), ): return nostrmarket_renderer().TemplateResponse( "nostrmarket/market.html", - {"request": request, "stall_id": stall_id, "product_id": product_id}, - ) - - -@nostrmarket_ext.get("/stall/{stall_id}", response_class=HTMLResponse) -async def stall(request: Request, stall_id: str): - return nostrmarket_renderer().TemplateResponse( - "nostrmarket/stall.html", - {"request": request, "stall_id": stall_id}, + { + "request": request, + "stall_id": stall_id, + "product_id": product_id, + "merchant_pubkey": merchant_pubkey, + }, ) From 692aae60afbaf73af9fe82f82658b2a9e56093e5 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Thu, 2 Mar 2023 15:36:42 +0000 Subject: [PATCH 095/891] fix naming --- .../customer-market/customer-market.html | 153 +++++++++--------- 1 file changed, 79 insertions(+), 74 deletions(-) diff --git a/static/components/customer-market/customer-market.html b/static/components/customer-market/customer-market.html index 99af972..b52c6c2 100644 --- a/static/components/customer-market/customer-market.html +++ b/static/components/customer-market/customer-market.html @@ -1,83 +1,88 @@ -
-
- - +
+ + + + + +
+
+ + - -
-
- {{ item.product }} + +
+
+ {{ item.name }} +
-
- - + + - -
-
- {{ item.stallName }} + +
+
+ {{ item.stallName }} +
+ + {{ item.price }} satsBTC {{ (item.price / 1e8).toFixed(8) }} + + + {{ item.formatedPrice }} + ({{ item.priceInSats }} sats) + + {{ item.amount }} left
- - {{ item.price }} satsBTC {{ (item.price / 1e8).toFixed(8) }} + {{cat}} - - - {{ item.formatedPrice }} - ({{ item.priceInSats }} sats) +
+

{{ item.description }}

+
+
+ + + + + Stall: {{ item.stallName }} +
+ See product + - - {{ item.amount }} left -
-
- {{cat}} -
-
-

{{ item.description }}

-
- - - - - - Stall: {{ item.stallName }} -
- See product - - Visit Stall - -
-
- + Visit Stall + +
+ + +
From 64b1bfc6d9e5648005efb479b695e3b5fdf0be94 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Thu, 2 Mar 2023 15:36:42 +0000 Subject: [PATCH 096/891] fix naming --- .../customer-market/customer-market.html | 153 +++++++++--------- 1 file changed, 79 insertions(+), 74 deletions(-) diff --git a/static/components/customer-market/customer-market.html b/static/components/customer-market/customer-market.html index 99af972..b52c6c2 100644 --- a/static/components/customer-market/customer-market.html +++ b/static/components/customer-market/customer-market.html @@ -1,83 +1,88 @@ -
-
- - +
+ + + + + +
+
+ + - -
-
- {{ item.product }} + +
+
+ {{ item.name }} +
-
- - + + - -
-
- {{ item.stallName }} + +
+
+ {{ item.stallName }} +
+ + {{ item.price }} satsBTC {{ (item.price / 1e8).toFixed(8) }} + + + {{ item.formatedPrice }} + ({{ item.priceInSats }} sats) + + {{ item.amount }} left
- - {{ item.price }} satsBTC {{ (item.price / 1e8).toFixed(8) }} + {{cat}} - - - {{ item.formatedPrice }} - ({{ item.priceInSats }} sats) +
+

{{ item.description }}

+
+
+ + + + + Stall: {{ item.stallName }} +
+ See product + - - {{ item.amount }} left -
-
- {{cat}} -
-
-

{{ item.description }}

-
- - - - - - Stall: {{ item.stallName }} -
- See product - - Visit Stall - -
-
- + Visit Stall + +
+ + +
From bd96e75f55cdaec5e862b24b74cbbe75aa727108 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Thu, 2 Mar 2023 15:37:21 +0000 Subject: [PATCH 097/891] add customer stall component, remove toolbar --- .../customer-stall/customer-stall.html | 102 ++++++++++++ .../customer-stall/customer-stall.js | 17 ++ templates/nostrmarket/market.html | 145 ++++++++++++++++-- 3 files changed, 249 insertions(+), 15 deletions(-) create mode 100644 static/components/customer-stall/customer-stall.html create mode 100644 static/components/customer-stall/customer-stall.js diff --git a/static/components/customer-stall/customer-stall.html b/static/components/customer-stall/customer-stall.html new file mode 100644 index 0000000..c99290e --- /dev/null +++ b/static/components/customer-stall/customer-stall.html @@ -0,0 +1,102 @@ +
+ + + + + + + + +
+
+ + + + + Add to cart +
+
+ {{ item.name }} +
+
+ + +
+ + +
+
+ {{ item.stallName }} +
+ + {{ item.price }} satsBTC {{ (item.price / 1e8).toFixed(8) }} + + + {{ item.formatedPrice }} + ({{ item.priceInSats }} sats) + + {{ item.amount }} left +
+
+ {{cat}} +
+
+

{{ item.description }}

+
+
+ + + + + Stall: {{ item.stallName }} +
+ See product +
+
+
+
+
+
diff --git a/static/components/customer-stall/customer-stall.js b/static/components/customer-stall/customer-stall.js new file mode 100644 index 0000000..5b4f778 --- /dev/null +++ b/static/components/customer-stall/customer-stall.js @@ -0,0 +1,17 @@ +async function customerStall(path) { + const template = await loadTemplateAsync(path) + Vue.component('customer-stall', { + name: 'customer-stall', + template, + + props: ['stall', 'products', 'exchange-rates'], + data: function () { + return {} + }, + methods: {}, + created() { + console.log(this.stall) + console.log(this.products) + } + }) +} diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html index dba048d..dc835cb 100644 --- a/templates/nostrmarket/market.html +++ b/templates/nostrmarket/market.html @@ -137,12 +137,50 @@ +
- + @@ -151,6 +189,7 @@ + + + diff --git a/views_api.py b/views_api.py index de83d66..472e17a 100644 --- a/views_api.py +++ b/views_api.py @@ -17,6 +17,7 @@ from lnbits.utils.exchange_rates import currencies from . import nostrmarket_ext from .crud import ( create_merchant, + create_product, create_stall, create_zone, delete_stall, @@ -29,7 +30,16 @@ from .crud import ( update_stall, update_zone, ) -from .models import Merchant, PartialMerchant, PartialStall, PartialZone, Stall, Zone +from .models import ( + Merchant, + PartialMerchant, + PartialProduct, + PartialStall, + PartialZone, + Product, + Stall, + Zone, +) from .nostr.nostr_client import publish_nostr_event ######################################## MERCHANT ######################################## @@ -170,6 +180,11 @@ async def api_create_stall( 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( @@ -202,6 +217,11 @@ async def api_update_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( @@ -271,10 +291,65 @@ async def api_delete_stall( logger.warning(ex) raise HTTPException( status_code=HTTPStatus.INTERNAL_SERVER_ERROR, - detail="Cannot delte stall", + detail="Cannot delete stall", ) +######################################## PRODUCTS ######################################## + + +@nostrmarket_ext.post("/api/v1/product") +async def api_market_product_create( + data: PartialProduct, + wallet: WalletTypeInfo = Depends(require_invoice_key), +) -> Product: + try: + data.validate_product() + product = await create_product(wallet.wallet.user, data=data) + + 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.get("/api/v1/product/{stall_id}") +# async def api_market_products( +# stall_id: str, wallet: WalletTypeInfo = Depends(require_invoice_key), +# ): +# wallet_ids = [wallet.wallet.id] + + +# return [product.dict() for product in await get_products(stalls)] + + +# @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) + + ######################################## OTHER ######################################## From dbf0cee6cbad42bc0de03b09472de4c926b4f3b9 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Thu, 2 Mar 2023 17:42:28 +0200 Subject: [PATCH 102/891] fat: add create product logic --- crud.py | 53 ++++++- migrations.py | 5 +- models.py | 42 +++++ .../shipping-zones/shipping-zones.html | 2 +- .../stall-details/stall-details.html | 148 +++++++++++++++++- .../components/stall-details/stall-details.js | 110 ++++++++++++- static/components/stall-list/stall-list.js | 14 +- static/js/utils.js | 9 ++ templates/nostrmarket/index.html | 1 + views_api.py | 79 +++++++++- 10 files changed, 446 insertions(+), 17 deletions(-) diff --git a/crud.py b/crud.py index 0f52c67..ce190bf 100644 --- a/crud.py +++ b/crud.py @@ -5,7 +5,16 @@ from typing import List, Optional from lnbits.helpers import urlsafe_short_hash from . import db -from .models import Merchant, PartialMerchant, PartialStall, PartialZone, Stall, Zone +from .models import ( + Merchant, + PartialMerchant, + PartialProduct, + PartialStall, + PartialZone, + Product, + Stall, + Zone, +) ######################################## MERCHANT ######################################## @@ -177,3 +186,45 @@ async def delete_stall(user_id: str, stall_id: str) -> None: stall_id, ), ) + + +######################################## STALL ######################################## + + +async def create_product(user_id: str, data: PartialProduct) -> Product: + product_id = urlsafe_short_hash() + + await db.execute( + f""" + INSERT INTO nostrmarket.products (user_id, id, stall_id, name, category_list, description, images, price, quantity) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + """, + ( + user_id, + product_id, + data.stall_id, + data.name, + json.dumps(data.categories), + data.description, + data.image, + data.price, + data.quantity, + ), + ) + product = await get_product(user_id, product_id) + assert product, "Newly created product couldn't be retrieved" + + return product + + +async def get_product(user_id: str, product_id: str) -> Optional[Product]: + row = await db.fetchone( + "SELECT * FROM nostrmarket.products WHERE user_id =? AND id = ?", + ( + user_id, + product_id, + ), + ) + product = Product.from_row(row) if row else None + + return product diff --git a/migrations.py b/migrations.py index 8af5726..2e0a158 100644 --- a/migrations.py +++ b/migrations.py @@ -39,12 +39,13 @@ async def m001_initial(db): await db.execute( f""" CREATE TABLE nostrmarket.products ( + user_id TEXT NOT NULL, id TEXT PRIMARY KEY, stall_id TEXT NOT NULL, name TEXT NOT NULL, - categories TEXT, + category_list TEXT DEFAULT '[]', description TEXT, - images TEXT NOT NULL DEFAULT '[]', + images TEXT DEFAULT '[]', price REAL NOT NULL, quantity INTEGER NOT NULL ); diff --git a/models.py b/models.py index 7892c93..b2bab4d 100644 --- a/models.py +++ b/models.py @@ -116,3 +116,45 @@ class Stall(PartialStall): stall.config = StallConfig(**json.loads(row["meta"])) stall.shipping_zones = [Zone(**z) for z in json.loads(row["zones"])] return stall + + +######################################## STALLS ######################################## + + +class PartialProduct(BaseModel): + stall_id: str + name: str + categories: List[str] = [] + description: Optional[str] + image: Optional[str] + price: float + quantity: int + + def validate_product(self): + if self.image: + image_is_url = self.image.startswith("https://") or self.image.startswith( + "http://" + ) + + if not image_is_url: + + def size(b64string): + return int((len(b64string) * 3) / 4 - b64string.count("=", -2)) + + image_size = size(self.image) / 1024 + if image_size > 100: + raise ValueError( + f""" + Image size is too big, {int(image_size)}Kb. + Max: 100kb, Compress the image at https://tinypng.com, or use an URL.""" + ) + + +class Product(PartialProduct): + id: str + + @classmethod + def from_row(cls, row: Row) -> "Product": + product = cls(**dict(row)) + product.categories = json.loads(row["category_list"]) + return product diff --git a/static/components/shipping-zones/shipping-zones.html b/static/components/shipping-zones/shipping-zones.html index 6984c6c..cedaca7 100644 --- a/static/components/shipping-zones/shipping-zones.html +++ b/static/components/shipping-zones/shipping-zones.html @@ -59,7 +59,7 @@
- -
+ +
+
+
+ New Product +
+
+
+
+
- +
+ + + + + + + + + + + + + + + + + + + + +
+ Update Product + + Create Product + + Cancel +
+
+
+
diff --git a/static/components/stall-details/stall-details.js b/static/components/stall-details/stall-details.js index 98f9bfb..dafd42a 100644 --- a/static/components/stall-details/stall-details.js +++ b/static/components/stall-details/stall-details.js @@ -1,6 +1,8 @@ async function stallDetails(path) { const template = await loadTemplateAsync(path) + const pica = window.pica() + Vue.component('stall-details', { name: 'stall-details', template, @@ -16,8 +18,21 @@ async function stallDetails(path) { data: function () { return { tab: 'info', - stall: null - // currencies: [], + stall: null, + products: [], + productDialog: { + showDialog: false, + url: true, + data: { + id: null, + name: '', + description: '', + categories: [], + image: null, + price: 0, + quantity: 0 + } + } } }, computed: { @@ -96,6 +111,97 @@ async function stallDetails(path) { LNbits.utils.notifyApiError(error) } }) + }, + imageAdded(file) { + const image = new Image() + image.src = URL.createObjectURL(file) + image.onload = async () => { + let fit = imgSizeFit(image) + let canvas = document.createElement('canvas') + canvas.setAttribute('width', fit.width) + canvas.setAttribute('height', fit.height) + output = await pica.resize(image, canvas) + this.productDialog.data.image = output.toDataURL('image/jpeg', 0.4) + this.productDialog = {...this.productDialog} + } + }, + imageCleared() { + this.productDialog.data.image = null + this.productDialog = {...this.productDialog} + }, + sendProductFormData: function () { + var data = { + stall_id: this.stall.id, + name: this.productDialog.data.name, + description: this.productDialog.data.description, + categories: this.productDialog.data.categories, + + image: this.productDialog.data.image, + price: this.productDialog.data.price, + quantity: this.productDialog.data.quantity + } + this.productDialog.showDialog = false + if (this.productDialog.data.id) { + this.updateProduct(data) + } else { + this.createProduct(data) + } + }, + updateProduct: function (data) { + var self = this + let wallet = _.findWhere(this.stalls, { + id: self.productDialog.data.stall + }).wallet + LNbits.api + .request( + 'PUT', + '/nostrmarket/api/v1/products/' + data.id, + _.findWhere(self.g.user.wallets, { + id: wallet + }).inkey, + data + ) + .then(async function (response) { + self.products = _.reject(self.products, function (obj) { + return obj.id == data.id + }) + let productData = mapProducts(response.data) + self.products.push(productData) + //SEND Nostr data + try { + await self.sendToRelays(productData, 'product', 'update') + } catch (e) { + console.error(e) + } + self.resetDialog('productDialog') + //self.productDialog.show = false + //self.productDialog.data = {} + }) + .catch(function (error) { + LNbits.utils.notifyApiError(error) + }) + }, + createProduct: async function (payload) { + try { + const {data} = await LNbits.api.request( + 'POST', + '/nostrmarket/api/v1/product', + this.adminkey, + payload + ) + this.products.unshift(data) + this.$q.notify({ + type: 'positive', + message: 'Product Created', + timeout: 5000 + }) + } catch (error) { + console.warn(error) + LNbits.utils.notifyApiError(error) + } + }, + showNewProductDialog: async function () { + this.productDialog.showDialog = true } }, created: async function () { diff --git a/static/components/stall-list/stall-list.js b/static/components/stall-list/stall-list.js index 7fb3f8c..5fd8ffd 100644 --- a/static/components/stall-list/stall-list.js +++ b/static/components/stall-list/stall-list.js @@ -35,12 +35,6 @@ async function stallList(path) { label: 'Name', field: 'id' }, - // { - // name: 'toggle', - // align: 'left', - // label: 'Active', - // field: '' - // }, { name: 'description', align: 'left', @@ -88,6 +82,7 @@ async function stallList(path) { stall ) this.stallDialog.show = false + data.expanded = false this.stalls.unshift(data) this.$q.notify({ type: 'positive', @@ -154,6 +149,13 @@ async function stallList(path) { openCreateStallDialog: async function () { await this.getCurrencies() await this.getZones() + if (!this.zoneOptions || !this.zoneOptions.length) { + this.$q.notify({ + type: 'warning', + message: 'Please create a Shipping Zone first!' + }) + return + } this.stallDialog.data = { name: '', description: '', diff --git a/static/js/utils.js b/static/js/utils.js index 11ebc81..83e886b 100644 --- a/static/js/utils.js +++ b/static/js/utils.js @@ -16,3 +16,12 @@ function loadTemplateAsync(path) { return result } + +function imgSizeFit(img, maxWidth = 1024, maxHeight = 768) { + let ratio = Math.min( + 1, + maxWidth / img.naturalWidth, + maxHeight / img.naturalHeight + ) + return {width: img.naturalWidth * ratio, height: img.naturalHeight * ratio} +} diff --git a/templates/nostrmarket/index.html b/templates/nostrmarket/index.html index ad9c94d..fa07f17 100644 --- a/templates/nostrmarket/index.html +++ b/templates/nostrmarket/index.html @@ -143,6 +143,7 @@ {% endblock%}{% block scripts %} {{ window_vars(user) }} + diff --git a/views_api.py b/views_api.py index de83d66..472e17a 100644 --- a/views_api.py +++ b/views_api.py @@ -17,6 +17,7 @@ from lnbits.utils.exchange_rates import currencies from . import nostrmarket_ext from .crud import ( create_merchant, + create_product, create_stall, create_zone, delete_stall, @@ -29,7 +30,16 @@ from .crud import ( update_stall, update_zone, ) -from .models import Merchant, PartialMerchant, PartialStall, PartialZone, Stall, Zone +from .models import ( + Merchant, + PartialMerchant, + PartialProduct, + PartialStall, + PartialZone, + Product, + Stall, + Zone, +) from .nostr.nostr_client import publish_nostr_event ######################################## MERCHANT ######################################## @@ -170,6 +180,11 @@ async def api_create_stall( 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( @@ -202,6 +217,11 @@ async def api_update_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( @@ -271,10 +291,65 @@ async def api_delete_stall( logger.warning(ex) raise HTTPException( status_code=HTTPStatus.INTERNAL_SERVER_ERROR, - detail="Cannot delte stall", + detail="Cannot delete stall", ) +######################################## PRODUCTS ######################################## + + +@nostrmarket_ext.post("/api/v1/product") +async def api_market_product_create( + data: PartialProduct, + wallet: WalletTypeInfo = Depends(require_invoice_key), +) -> Product: + try: + data.validate_product() + product = await create_product(wallet.wallet.user, data=data) + + 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.get("/api/v1/product/{stall_id}") +# async def api_market_products( +# stall_id: str, wallet: WalletTypeInfo = Depends(require_invoice_key), +# ): +# wallet_ids = [wallet.wallet.id] + + +# return [product.dict() for product in await get_products(stalls)] + + +# @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) + + ######################################## OTHER ######################################## From e17cea65cbd4dee6fb3e56512127b3f915a4337b Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Thu, 2 Mar 2023 17:42:50 +0200 Subject: [PATCH 103/891] chore: code clean-up --- static/components/stall-details/stall-details.html | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/static/components/stall-details/stall-details.html b/static/components/stall-details/stall-details.html index e0d9b3b..73615c9 100644 --- a/static/components/stall-details/stall-details.html +++ b/static/components/stall-details/stall-details.html @@ -137,19 +137,6 @@ v-model.trim="productDialog.data.description" label="Description" > - - - Date: Thu, 2 Mar 2023 17:42:50 +0200 Subject: [PATCH 104/891] chore: code clean-up --- static/components/stall-details/stall-details.html | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/static/components/stall-details/stall-details.html b/static/components/stall-details/stall-details.html index e0d9b3b..73615c9 100644 --- a/static/components/stall-details/stall-details.html +++ b/static/components/stall-details/stall-details.html @@ -137,19 +137,6 @@ v-model.trim="productDialog.data.description" label="Description" > - - - Date: Thu, 2 Mar 2023 18:05:49 +0200 Subject: [PATCH 105/891] feat: add products table --- crud.py | 8 +++ .../stall-details/stall-details.html | 53 ++++++++++++++ .../components/stall-details/stall-details.js | 72 +++++++++++++++++++ views_api.py | 27 ++++--- 4 files changed, 150 insertions(+), 10 deletions(-) diff --git a/crud.py b/crud.py index ce190bf..d19a958 100644 --- a/crud.py +++ b/crud.py @@ -228,3 +228,11 @@ async def get_product(user_id: str, product_id: str) -> Optional[Product]: product = Product.from_row(row) if row else None return product + + +async def get_products(user_id: str, stall_id: str) -> List[Product]: + rows = await db.fetchall( + "SELECT * FROM nostrmarket.products WHERE user_id = ? AND stall_id = ?", + (user_id, stall_id), + ) + return [Product.from_row(row) for row in rows] diff --git a/static/components/stall-details/stall-details.html b/static/components/stall-details/stall-details.html index 73615c9..a4260b2 100644 --- a/static/components/stall-details/stall-details.html +++ b/static/components/stall-details/stall-details.html @@ -115,6 +115,59 @@
+ +
+
+ + + +
+
diff --git a/static/components/stall-details/stall-details.js b/static/components/stall-details/stall-details.js index dafd42a..9fead9e 100644 --- a/static/components/stall-details/stall-details.js +++ b/static/components/stall-details/stall-details.js @@ -32,6 +32,63 @@ async function stallDetails(path) { price: 0, quantity: 0 } + }, + productsFilter: '', + productsTable: { + columns: [ + { + name: 'delete', + align: 'left', + label: '', + field: '' + }, + { + name: 'edit', + align: 'left', + label: '', + field: '' + }, + + { + name: 'id', + align: 'left', + label: 'ID', + field: 'id' + }, + { + name: 'name', + align: 'left', + label: 'Name', + field: 'name' + }, + { + name: 'price', + align: 'left', + label: 'Price', + field: 'price' + }, + { + name: 'quantity', + align: 'left', + label: 'Quantity', + field: 'quantity' + }, + { + name: 'categories', + align: 'left', + label: 'Categories', + field: 'categories' + }, + { + name: 'description', + align: 'left', + label: 'Description', + field: 'description' + } + ], + pagination: { + rowsPerPage: 10 + } } } }, @@ -129,6 +186,20 @@ async function stallDetails(path) { this.productDialog.data.image = null this.productDialog = {...this.productDialog} }, + getProducts: async function () { + try { + const {data} = await LNbits.api.request( + 'GET', + '/nostrmarket/api/v1/product/' + this.stall.id, + this.inkey + ) + this.products = data + + console.log('### this.products', this.products) + } catch (error) { + LNbits.utils.notifyApiError(error) + } + }, sendProductFormData: function () { var data = { stall_id: this.stall.id, @@ -206,6 +277,7 @@ async function stallDetails(path) { }, created: async function () { await this.getStall() + await this.getProducts() } }) } diff --git a/views_api.py b/views_api.py index 472e17a..76f4f11 100644 --- a/views_api.py +++ b/views_api.py @@ -23,6 +23,7 @@ from .crud import ( delete_stall, delete_zone, get_merchant_for_user, + get_products, get_stall, get_stalls, get_zone, @@ -299,9 +300,9 @@ async def api_delete_stall( @nostrmarket_ext.post("/api/v1/product") -async def api_market_product_create( +async def api_create_product( data: PartialProduct, - wallet: WalletTypeInfo = Depends(require_invoice_key), + wallet: WalletTypeInfo = Depends(require_admin_key), ) -> Product: try: data.validate_product() @@ -321,14 +322,20 @@ async def api_market_product_create( ) -# @nostrmarket_ext.get("/api/v1/product/{stall_id}") -# async def api_market_products( -# stall_id: str, wallet: WalletTypeInfo = Depends(require_invoice_key), -# ): -# wallet_ids = [wallet.wallet.id] - - -# return [product.dict() for product in await get_products(stalls)] +@nostrmarket_ext.get("/api/v1/product/{stall_id}") +async def api_get_product( + 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 product", + ) # @market_ext.delete("/api/v1/products/{product_id}") From e32cebffad40dc8555e56fa198c54d535dbea7ef Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Thu, 2 Mar 2023 18:05:49 +0200 Subject: [PATCH 106/891] feat: add products table --- crud.py | 8 +++ .../stall-details/stall-details.html | 53 ++++++++++++++ .../components/stall-details/stall-details.js | 72 +++++++++++++++++++ views_api.py | 27 ++++--- 4 files changed, 150 insertions(+), 10 deletions(-) diff --git a/crud.py b/crud.py index ce190bf..d19a958 100644 --- a/crud.py +++ b/crud.py @@ -228,3 +228,11 @@ async def get_product(user_id: str, product_id: str) -> Optional[Product]: product = Product.from_row(row) if row else None return product + + +async def get_products(user_id: str, stall_id: str) -> List[Product]: + rows = await db.fetchall( + "SELECT * FROM nostrmarket.products WHERE user_id = ? AND stall_id = ?", + (user_id, stall_id), + ) + return [Product.from_row(row) for row in rows] diff --git a/static/components/stall-details/stall-details.html b/static/components/stall-details/stall-details.html index 73615c9..a4260b2 100644 --- a/static/components/stall-details/stall-details.html +++ b/static/components/stall-details/stall-details.html @@ -115,6 +115,59 @@
+ +
+
+ + + +
+
diff --git a/static/components/stall-details/stall-details.js b/static/components/stall-details/stall-details.js index dafd42a..9fead9e 100644 --- a/static/components/stall-details/stall-details.js +++ b/static/components/stall-details/stall-details.js @@ -32,6 +32,63 @@ async function stallDetails(path) { price: 0, quantity: 0 } + }, + productsFilter: '', + productsTable: { + columns: [ + { + name: 'delete', + align: 'left', + label: '', + field: '' + }, + { + name: 'edit', + align: 'left', + label: '', + field: '' + }, + + { + name: 'id', + align: 'left', + label: 'ID', + field: 'id' + }, + { + name: 'name', + align: 'left', + label: 'Name', + field: 'name' + }, + { + name: 'price', + align: 'left', + label: 'Price', + field: 'price' + }, + { + name: 'quantity', + align: 'left', + label: 'Quantity', + field: 'quantity' + }, + { + name: 'categories', + align: 'left', + label: 'Categories', + field: 'categories' + }, + { + name: 'description', + align: 'left', + label: 'Description', + field: 'description' + } + ], + pagination: { + rowsPerPage: 10 + } } } }, @@ -129,6 +186,20 @@ async function stallDetails(path) { this.productDialog.data.image = null this.productDialog = {...this.productDialog} }, + getProducts: async function () { + try { + const {data} = await LNbits.api.request( + 'GET', + '/nostrmarket/api/v1/product/' + this.stall.id, + this.inkey + ) + this.products = data + + console.log('### this.products', this.products) + } catch (error) { + LNbits.utils.notifyApiError(error) + } + }, sendProductFormData: function () { var data = { stall_id: this.stall.id, @@ -206,6 +277,7 @@ async function stallDetails(path) { }, created: async function () { await this.getStall() + await this.getProducts() } }) } diff --git a/views_api.py b/views_api.py index 472e17a..76f4f11 100644 --- a/views_api.py +++ b/views_api.py @@ -23,6 +23,7 @@ from .crud import ( delete_stall, delete_zone, get_merchant_for_user, + get_products, get_stall, get_stalls, get_zone, @@ -299,9 +300,9 @@ async def api_delete_stall( @nostrmarket_ext.post("/api/v1/product") -async def api_market_product_create( +async def api_create_product( data: PartialProduct, - wallet: WalletTypeInfo = Depends(require_invoice_key), + wallet: WalletTypeInfo = Depends(require_admin_key), ) -> Product: try: data.validate_product() @@ -321,14 +322,20 @@ async def api_market_product_create( ) -# @nostrmarket_ext.get("/api/v1/product/{stall_id}") -# async def api_market_products( -# stall_id: str, wallet: WalletTypeInfo = Depends(require_invoice_key), -# ): -# wallet_ids = [wallet.wallet.id] - - -# return [product.dict() for product in await get_products(stalls)] +@nostrmarket_ext.get("/api/v1/product/{stall_id}") +async def api_get_product( + 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 product", + ) # @market_ext.delete("/api/v1/products/{product_id}") From 1e6aaf84368e9744df6e77765b3e19a6f52539c1 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Thu, 2 Mar 2023 18:15:17 +0200 Subject: [PATCH 107/891] feat: delete product --- crud.py | 11 +++++++ .../stall-details/stall-details.html | 2 +- .../components/stall-details/stall-details.js | 26 +++++++++++++++- views_api.py | 31 +++++++++---------- 4 files changed, 51 insertions(+), 19 deletions(-) diff --git a/crud.py b/crud.py index d19a958..a89455e 100644 --- a/crud.py +++ b/crud.py @@ -106,6 +106,7 @@ async def get_zones(user_id: str) -> List[Zone]: async def delete_zone(zone_id: str) -> None: + # todo: add user_id await db.execute("DELETE FROM nostrmarket.zones WHERE id = ?", (zone_id,)) @@ -236,3 +237,13 @@ async def get_products(user_id: str, stall_id: str) -> List[Product]: (user_id, stall_id), ) return [Product.from_row(row) for row in rows] + + +async def delete_product(user_id: str, product_id: str) -> None: + await db.execute( + "DELETE FROM nostrmarket.products WHERE user_id =? AND id = ?", + ( + user_id, + product_id, + ), + ) diff --git a/static/components/stall-details/stall-details.html b/static/components/stall-details/stall-details.html index a4260b2..b2c736a 100644 --- a/static/components/stall-details/stall-details.html +++ b/static/components/stall-details/stall-details.html @@ -134,7 +134,7 @@ size="sm" color="pink" dense - @click="props.row.expanded= !props.row.expanded" + @click="deleteProduct(props.row.id)" icon="delete" />
diff --git a/static/components/stall-details/stall-details.js b/static/components/stall-details/stall-details.js index 9fead9e..ac86980 100644 --- a/static/components/stall-details/stall-details.js +++ b/static/components/stall-details/stall-details.js @@ -17,7 +17,7 @@ async function stallDetails(path) { ], data: function () { return { - tab: 'info', + tab: 'products', stall: null, products: [], productDialog: { @@ -271,6 +271,30 @@ async function stallDetails(path) { LNbits.utils.notifyApiError(error) } }, + deleteProduct: async function (productId) { + LNbits.utils + .confirmDialog('Are you sure you want to delete this product?') + .onOk(async () => { + try { + await LNbits.api.request( + 'DELETE', + '/nostrmarket/api/v1/product/' + productId, + this.adminkey + ) + this.products = _.reject(this.products, function (obj) { + return obj.id === productId + }) + this.$q.notify({ + type: 'positive', + message: 'Product deleted', + timeout: 5000 + }) + } catch (error) { + console.warn(error) + LNbits.utils.notifyApiError(error) + } + }) + }, showNewProductDialog: async function () { this.productDialog.showDialog = true } diff --git a/views_api.py b/views_api.py index 76f4f11..9260c9a 100644 --- a/views_api.py +++ b/views_api.py @@ -20,6 +20,7 @@ from .crud import ( create_product, create_stall, create_zone, + delete_product, delete_stall, delete_zone, get_merchant_for_user, @@ -338,23 +339,19 @@ async def api_get_product( ) -# @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) +@nostrmarket_ext.delete("/api/v1/product/{product_id}") +async def api_delete_product( + product_id: str, + wallet: WalletTypeInfo = Depends(require_admin_key), +): + try: + await delete_product(wallet.wallet.user, product_id) + except Exception as ex: + logger.warning(ex) + raise HTTPException( + status_code=HTTPStatus.INTERNAL_SERVER_ERROR, + detail="Cannot delete product", + ) ######################################## OTHER ######################################## From 48d6b44d0c014e0ed29b4fda262b4f26208f0e58 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Thu, 2 Mar 2023 18:15:17 +0200 Subject: [PATCH 108/891] feat: delete product --- crud.py | 11 +++++++ .../stall-details/stall-details.html | 2 +- .../components/stall-details/stall-details.js | 26 +++++++++++++++- views_api.py | 31 +++++++++---------- 4 files changed, 51 insertions(+), 19 deletions(-) diff --git a/crud.py b/crud.py index d19a958..a89455e 100644 --- a/crud.py +++ b/crud.py @@ -106,6 +106,7 @@ async def get_zones(user_id: str) -> List[Zone]: async def delete_zone(zone_id: str) -> None: + # todo: add user_id await db.execute("DELETE FROM nostrmarket.zones WHERE id = ?", (zone_id,)) @@ -236,3 +237,13 @@ async def get_products(user_id: str, stall_id: str) -> List[Product]: (user_id, stall_id), ) return [Product.from_row(row) for row in rows] + + +async def delete_product(user_id: str, product_id: str) -> None: + await db.execute( + "DELETE FROM nostrmarket.products WHERE user_id =? AND id = ?", + ( + user_id, + product_id, + ), + ) diff --git a/static/components/stall-details/stall-details.html b/static/components/stall-details/stall-details.html index a4260b2..b2c736a 100644 --- a/static/components/stall-details/stall-details.html +++ b/static/components/stall-details/stall-details.html @@ -134,7 +134,7 @@ size="sm" color="pink" dense - @click="props.row.expanded= !props.row.expanded" + @click="deleteProduct(props.row.id)" icon="delete" /> diff --git a/static/components/stall-details/stall-details.js b/static/components/stall-details/stall-details.js index 9fead9e..ac86980 100644 --- a/static/components/stall-details/stall-details.js +++ b/static/components/stall-details/stall-details.js @@ -17,7 +17,7 @@ async function stallDetails(path) { ], data: function () { return { - tab: 'info', + tab: 'products', stall: null, products: [], productDialog: { @@ -271,6 +271,30 @@ async function stallDetails(path) { LNbits.utils.notifyApiError(error) } }, + deleteProduct: async function (productId) { + LNbits.utils + .confirmDialog('Are you sure you want to delete this product?') + .onOk(async () => { + try { + await LNbits.api.request( + 'DELETE', + '/nostrmarket/api/v1/product/' + productId, + this.adminkey + ) + this.products = _.reject(this.products, function (obj) { + return obj.id === productId + }) + this.$q.notify({ + type: 'positive', + message: 'Product deleted', + timeout: 5000 + }) + } catch (error) { + console.warn(error) + LNbits.utils.notifyApiError(error) + } + }) + }, showNewProductDialog: async function () { this.productDialog.showDialog = true } diff --git a/views_api.py b/views_api.py index 76f4f11..9260c9a 100644 --- a/views_api.py +++ b/views_api.py @@ -20,6 +20,7 @@ from .crud import ( create_product, create_stall, create_zone, + delete_product, delete_stall, delete_zone, get_merchant_for_user, @@ -338,23 +339,19 @@ async def api_get_product( ) -# @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) +@nostrmarket_ext.delete("/api/v1/product/{product_id}") +async def api_delete_product( + product_id: str, + wallet: WalletTypeInfo = Depends(require_admin_key), +): + try: + await delete_product(wallet.wallet.user, product_id) + except Exception as ex: + logger.warning(ex) + raise HTTPException( + status_code=HTTPStatus.INTERNAL_SERVER_ERROR, + detail="Cannot delete product", + ) ######################################## OTHER ######################################## From 62e7d439c7489939f20bb8680a5f3004f168c282 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Thu, 2 Mar 2023 18:38:06 +0200 Subject: [PATCH 109/891] feat: update product --- crud.py | 28 +++++++- .../stall-details/stall-details.html | 2 +- .../components/stall-details/stall-details.js | 64 ++++++++++--------- views_api.py | 25 ++++++++ 4 files changed, 84 insertions(+), 35 deletions(-) diff --git a/crud.py b/crud.py index a89455e..996bfc3 100644 --- a/crud.py +++ b/crud.py @@ -218,6 +218,30 @@ async def create_product(user_id: str, data: PartialProduct) -> Product: return product +async def update_product(user_id: str, product: Product) -> Product: + + await db.execute( + f""" + UPDATE nostrmarket.products set name = ?, category_list = ?, description = ?, images = ?, price = ?, quantity = ? + WHERE user_id = ? AND id = ? + """, + ( + product.name, + json.dumps(product.categories), + product.description, + product.image, + product.price, + product.quantity, + user_id, + product.id, + ), + ) + updated_product = await get_product(user_id, product.id) + assert updated_product, "Updated product couldn't be retrieved" + + return updated_product + + async def get_product(user_id: str, product_id: str) -> Optional[Product]: row = await db.fetchone( "SELECT * FROM nostrmarket.products WHERE user_id =? AND id = ?", @@ -226,9 +250,7 @@ async def get_product(user_id: str, product_id: str) -> Optional[Product]: product_id, ), ) - product = Product.from_row(row) if row else None - - return product + return Product.from_row(row) if row else None async def get_products(user_id: str, stall_id: str) -> List[Product]: diff --git a/static/components/stall-details/stall-details.html b/static/components/stall-details/stall-details.html index b2c736a..82610ca 100644 --- a/static/components/stall-details/stall-details.html +++ b/static/components/stall-details/stall-details.html @@ -143,7 +143,7 @@ size="sm" color="accent" dense - @click="props.row.expanded= !props.row.expanded" + @click="editProduct(props.row)" icon="edit" /> diff --git a/static/components/stall-details/stall-details.js b/static/components/stall-details/stall-details.js index ac86980..e62caaf 100644 --- a/static/components/stall-details/stall-details.js +++ b/static/components/stall-details/stall-details.js @@ -203,6 +203,7 @@ async function stallDetails(path) { sendProductFormData: function () { var data = { stall_id: this.stall.id, + id: this.productDialog.data.id, name: this.productDialog.data.name, description: this.productDialog.data.description, categories: this.productDialog.data.categories, @@ -218,39 +219,27 @@ async function stallDetails(path) { this.createProduct(data) } }, - updateProduct: function (data) { - var self = this - let wallet = _.findWhere(this.stalls, { - id: self.productDialog.data.stall - }).wallet - LNbits.api - .request( - 'PUT', - '/nostrmarket/api/v1/products/' + data.id, - _.findWhere(self.g.user.wallets, { - id: wallet - }).inkey, - data + updateProduct: async function (product) { + try { + const {data} = await LNbits.api.request( + 'PATCH', + '/nostrmarket/api/v1/product/' + product.id, + this.adminkey, + product ) - .then(async function (response) { - self.products = _.reject(self.products, function (obj) { - return obj.id == data.id - }) - let productData = mapProducts(response.data) - self.products.push(productData) - //SEND Nostr data - try { - await self.sendToRelays(productData, 'product', 'update') - } catch (e) { - console.error(e) - } - self.resetDialog('productDialog') - //self.productDialog.show = false - //self.productDialog.data = {} - }) - .catch(function (error) { - LNbits.utils.notifyApiError(error) + const index = this.products.findIndex(r => r.id === product.id) + if (index !== -1) { + this.products.splice(index, 1, data) + } + this.$q.notify({ + type: 'positive', + message: 'Product Updated', + timeout: 5000 }) + } catch (error) { + console.warn(error) + LNbits.utils.notifyApiError(error) + } }, createProduct: async function (payload) { try { @@ -271,6 +260,10 @@ async function stallDetails(path) { LNbits.utils.notifyApiError(error) } }, + editProduct: async function (product) { + this.productDialog.data = {...product} + this.productDialog.showDialog = true + }, deleteProduct: async function (productId) { LNbits.utils .confirmDialog('Are you sure you want to delete this product?') @@ -296,6 +289,15 @@ async function stallDetails(path) { }) }, showNewProductDialog: async function () { + this.productDialog.data = { + id: null, + name: '', + description: '', + categories: [], + image: null, + price: 0, + quantity: 0 + } this.productDialog.showDialog = true } }, diff --git a/views_api.py b/views_api.py index 9260c9a..3de3f97 100644 --- a/views_api.py +++ b/views_api.py @@ -29,6 +29,7 @@ from .crud import ( get_stalls, get_zone, get_zones, + update_product, update_stall, update_zone, ) @@ -323,6 +324,30 @@ async def api_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: + product.validate_product() + product = 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/{stall_id}") async def api_get_product( stall_id: str, From 71739afc0a1c73d9d02a5033d54a8dade811419c Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Thu, 2 Mar 2023 18:38:06 +0200 Subject: [PATCH 110/891] feat: update product --- crud.py | 28 +++++++- .../stall-details/stall-details.html | 2 +- .../components/stall-details/stall-details.js | 64 ++++++++++--------- views_api.py | 25 ++++++++ 4 files changed, 84 insertions(+), 35 deletions(-) diff --git a/crud.py b/crud.py index a89455e..996bfc3 100644 --- a/crud.py +++ b/crud.py @@ -218,6 +218,30 @@ async def create_product(user_id: str, data: PartialProduct) -> Product: return product +async def update_product(user_id: str, product: Product) -> Product: + + await db.execute( + f""" + UPDATE nostrmarket.products set name = ?, category_list = ?, description = ?, images = ?, price = ?, quantity = ? + WHERE user_id = ? AND id = ? + """, + ( + product.name, + json.dumps(product.categories), + product.description, + product.image, + product.price, + product.quantity, + user_id, + product.id, + ), + ) + updated_product = await get_product(user_id, product.id) + assert updated_product, "Updated product couldn't be retrieved" + + return updated_product + + async def get_product(user_id: str, product_id: str) -> Optional[Product]: row = await db.fetchone( "SELECT * FROM nostrmarket.products WHERE user_id =? AND id = ?", @@ -226,9 +250,7 @@ async def get_product(user_id: str, product_id: str) -> Optional[Product]: product_id, ), ) - product = Product.from_row(row) if row else None - - return product + return Product.from_row(row) if row else None async def get_products(user_id: str, stall_id: str) -> List[Product]: diff --git a/static/components/stall-details/stall-details.html b/static/components/stall-details/stall-details.html index b2c736a..82610ca 100644 --- a/static/components/stall-details/stall-details.html +++ b/static/components/stall-details/stall-details.html @@ -143,7 +143,7 @@ size="sm" color="accent" dense - @click="props.row.expanded= !props.row.expanded" + @click="editProduct(props.row)" icon="edit" /> diff --git a/static/components/stall-details/stall-details.js b/static/components/stall-details/stall-details.js index ac86980..e62caaf 100644 --- a/static/components/stall-details/stall-details.js +++ b/static/components/stall-details/stall-details.js @@ -203,6 +203,7 @@ async function stallDetails(path) { sendProductFormData: function () { var data = { stall_id: this.stall.id, + id: this.productDialog.data.id, name: this.productDialog.data.name, description: this.productDialog.data.description, categories: this.productDialog.data.categories, @@ -218,39 +219,27 @@ async function stallDetails(path) { this.createProduct(data) } }, - updateProduct: function (data) { - var self = this - let wallet = _.findWhere(this.stalls, { - id: self.productDialog.data.stall - }).wallet - LNbits.api - .request( - 'PUT', - '/nostrmarket/api/v1/products/' + data.id, - _.findWhere(self.g.user.wallets, { - id: wallet - }).inkey, - data + updateProduct: async function (product) { + try { + const {data} = await LNbits.api.request( + 'PATCH', + '/nostrmarket/api/v1/product/' + product.id, + this.adminkey, + product ) - .then(async function (response) { - self.products = _.reject(self.products, function (obj) { - return obj.id == data.id - }) - let productData = mapProducts(response.data) - self.products.push(productData) - //SEND Nostr data - try { - await self.sendToRelays(productData, 'product', 'update') - } catch (e) { - console.error(e) - } - self.resetDialog('productDialog') - //self.productDialog.show = false - //self.productDialog.data = {} - }) - .catch(function (error) { - LNbits.utils.notifyApiError(error) + const index = this.products.findIndex(r => r.id === product.id) + if (index !== -1) { + this.products.splice(index, 1, data) + } + this.$q.notify({ + type: 'positive', + message: 'Product Updated', + timeout: 5000 }) + } catch (error) { + console.warn(error) + LNbits.utils.notifyApiError(error) + } }, createProduct: async function (payload) { try { @@ -271,6 +260,10 @@ async function stallDetails(path) { LNbits.utils.notifyApiError(error) } }, + editProduct: async function (product) { + this.productDialog.data = {...product} + this.productDialog.showDialog = true + }, deleteProduct: async function (productId) { LNbits.utils .confirmDialog('Are you sure you want to delete this product?') @@ -296,6 +289,15 @@ async function stallDetails(path) { }) }, showNewProductDialog: async function () { + this.productDialog.data = { + id: null, + name: '', + description: '', + categories: [], + image: null, + price: 0, + quantity: 0 + } this.productDialog.showDialog = true } }, diff --git a/views_api.py b/views_api.py index 9260c9a..3de3f97 100644 --- a/views_api.py +++ b/views_api.py @@ -29,6 +29,7 @@ from .crud import ( get_stalls, get_zone, get_zones, + update_product, update_stall, update_zone, ) @@ -323,6 +324,30 @@ async def api_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: + product.validate_product() + product = 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/{stall_id}") async def api_get_product( stall_id: str, From 8ce68f38f6825ad6852acb6f8e8cad2f0311f00d Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Thu, 2 Mar 2023 18:45:27 +0200 Subject: [PATCH 111/891] feat: add `to_nostr_event` for product --- models.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/models.py b/models.py index b2bab4d..412bbfc 100644 --- a/models.py +++ b/models.py @@ -153,6 +153,28 @@ class PartialProduct(BaseModel): class Product(PartialProduct): id: str + def to_nostr_event(self, pubkey: str) -> NostrEvent: + content = { + "stall_id": self.stall_id, + "name": self.name, + "description": self.description, + "image": self.image, + "price": self.price, + "quantity": self.quantity, + } + categories = [["t", tag] for tag in self.categories] + + event = NostrEvent( + pubkey=pubkey, + created_at=round(time.time()), + kind=30005, + tags=[["d", self.id]] + categories, + content=json.dumps(content, separators=(",", ":"), ensure_ascii=False), + ) + event.id = event.event_id + + return event + @classmethod def from_row(cls, row: Row) -> "Product": product = cls(**dict(row)) From d261b0c7f307726271c43f5b9aa7114f9ed14f71 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Thu, 2 Mar 2023 18:45:27 +0200 Subject: [PATCH 112/891] feat: add `to_nostr_event` for product --- models.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/models.py b/models.py index b2bab4d..412bbfc 100644 --- a/models.py +++ b/models.py @@ -153,6 +153,28 @@ class PartialProduct(BaseModel): class Product(PartialProduct): id: str + def to_nostr_event(self, pubkey: str) -> NostrEvent: + content = { + "stall_id": self.stall_id, + "name": self.name, + "description": self.description, + "image": self.image, + "price": self.price, + "quantity": self.quantity, + } + categories = [["t", tag] for tag in self.categories] + + event = NostrEvent( + pubkey=pubkey, + created_at=round(time.time()), + kind=30005, + tags=[["d", self.id]] + categories, + content=json.dumps(content, separators=(",", ":"), ensure_ascii=False), + ) + event.id = event.event_id + + return event + @classmethod def from_row(cls, row: Row) -> "Product": product = cls(**dict(row)) From 54978c847f83f7f9b9d82cef7592fbb33b52fc91 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Fri, 3 Mar 2023 11:08:55 +0200 Subject: [PATCH 113/891] feat: publish events for products --- crud.py | 12 ++-- migrations.py | 8 +-- models.py | 41 +++++++++-- .../stall-details/stall-details.html | 2 +- .../components/stall-details/stall-details.js | 10 +-- views_api.py | 68 +++++++++++++++++-- 6 files changed, 117 insertions(+), 24 deletions(-) diff --git a/crud.py b/crud.py index 996bfc3..f4d85c5 100644 --- a/crud.py +++ b/crud.py @@ -197,7 +197,7 @@ async def create_product(user_id: str, data: PartialProduct) -> Product: await db.execute( f""" - INSERT INTO nostrmarket.products (user_id, id, stall_id, name, category_list, description, images, price, quantity) + INSERT INTO nostrmarket.products (user_id, id, stall_id, name, images, price, quantity, category_list, meta) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( @@ -205,11 +205,11 @@ async def create_product(user_id: str, data: PartialProduct) -> Product: product_id, data.stall_id, data.name, - json.dumps(data.categories), - data.description, data.image, data.price, data.quantity, + json.dumps(data.categories), + json.dumps(data.config.dict()), ), ) product = await get_product(user_id, product_id) @@ -222,18 +222,18 @@ async def update_product(user_id: str, product: Product) -> Product: await db.execute( f""" - UPDATE nostrmarket.products set name = ?, category_list = ?, description = ?, images = ?, price = ?, quantity = ? + UPDATE nostrmarket.products set name = ?, description = ?, images = ?, price = ?, quantity = ?, category_list = ?, meta = ? WHERE user_id = ? AND id = ? """, ( product.name, - json.dumps(product.categories), - product.description, product.image, product.price, product.quantity, user_id, product.id, + json.dumps(product.categories), + json.dumps(product.config), ), ) updated_product = await get_product(user_id, product.id) diff --git a/migrations.py b/migrations.py index 2e0a158..696e584 100644 --- a/migrations.py +++ b/migrations.py @@ -37,17 +37,17 @@ async def m001_initial(db): Initial products table. """ await db.execute( - f""" + """ CREATE TABLE nostrmarket.products ( user_id TEXT NOT NULL, id TEXT PRIMARY KEY, stall_id TEXT NOT NULL, name TEXT NOT NULL, - category_list TEXT DEFAULT '[]', - description TEXT, images TEXT DEFAULT '[]', price REAL NOT NULL, - quantity INTEGER NOT NULL + quantity INTEGER NOT NULL, + category_list TEXT DEFAULT '[]', + meta TEXT NOT NULL DEFAULT '{}' ); """ ) diff --git a/models.py b/models.py index 412bbfc..0becfe3 100644 --- a/models.py +++ b/models.py @@ -1,5 +1,6 @@ import json import time +from abc import abstractmethod from sqlite3 import Row from typing import List, Optional @@ -8,6 +9,18 @@ from pydantic import BaseModel from .helpers import sign_message_hash from .nostr.event import NostrEvent +######################################## NOSTR ######################################## + + +class Nostrable: + @abstractmethod + def to_nostr_event(self, pubkey: str) -> NostrEvent: + pass + + @abstractmethod + def to_nostr_delete_event(self, pubkey: str) -> NostrEvent: + pass + ######################################## MERCHANT ######################################## class MerchantConfig(BaseModel): @@ -90,7 +103,7 @@ class Stall(PartialStall): event = NostrEvent( pubkey=pubkey, created_at=round(time.time()), - kind=30005, + kind=30017, tags=[["d", self.id]], content=json.dumps(content, separators=(",", ":"), ensure_ascii=False), ) @@ -121,14 +134,19 @@ class Stall(PartialStall): ######################################## STALLS ######################################## +class ProductConfig(BaseModel): + event_id: Optional[str] + description: Optional[str] + + class PartialProduct(BaseModel): stall_id: str name: str categories: List[str] = [] - description: Optional[str] image: Optional[str] price: float quantity: int + config: ProductConfig = ProductConfig() def validate_product(self): if self.image: @@ -150,14 +168,14 @@ class PartialProduct(BaseModel): ) -class Product(PartialProduct): +class Product(PartialProduct, Nostrable): id: str def to_nostr_event(self, pubkey: str) -> NostrEvent: content = { "stall_id": self.stall_id, "name": self.name, - "description": self.description, + "description": self.config.description, "image": self.image, "price": self.price, "quantity": self.quantity, @@ -167,7 +185,7 @@ class Product(PartialProduct): event = NostrEvent( pubkey=pubkey, created_at=round(time.time()), - kind=30005, + kind=30018, tags=[["d", self.id]] + categories, content=json.dumps(content, separators=(",", ":"), ensure_ascii=False), ) @@ -175,8 +193,21 @@ class Product(PartialProduct): return event + def to_nostr_delete_event(self, pubkey: str) -> NostrEvent: + delete_event = NostrEvent( + pubkey=pubkey, + created_at=round(time.time()), + kind=5, + tags=[["e", self.config.event_id]], + content=f"Product '{self.name}' deleted", + ) + delete_event.id = delete_event.event_id + + return delete_event + @classmethod def from_row(cls, row: Row) -> "Product": product = cls(**dict(row)) + product.config = ProductConfig(**json.loads(row["meta"])) product.categories = json.loads(row["category_list"]) return product diff --git a/static/components/stall-details/stall-details.html b/static/components/stall-details/stall-details.html index 82610ca..9246aad 100644 --- a/static/components/stall-details/stall-details.html +++ b/static/components/stall-details/stall-details.html @@ -161,7 +161,7 @@ - {{props.row.description}} + {{props.row.config.description}} diff --git a/static/components/stall-details/stall-details.js b/static/components/stall-details/stall-details.js index e62caaf..b8ef9c8 100644 --- a/static/components/stall-details/stall-details.js +++ b/static/components/stall-details/stall-details.js @@ -190,7 +190,7 @@ async function stallDetails(path) { try { const {data} = await LNbits.api.request( 'GET', - '/nostrmarket/api/v1/product/' + this.stall.id, + '/nostrmarket/api/v1/stall/product/' + this.stall.id, this.inkey ) this.products = data @@ -205,12 +205,14 @@ async function stallDetails(path) { stall_id: this.stall.id, id: this.productDialog.data.id, name: this.productDialog.data.name, - description: this.productDialog.data.description, - categories: this.productDialog.data.categories, image: this.productDialog.data.image, price: this.productDialog.data.price, - quantity: this.productDialog.data.quantity + quantity: this.productDialog.data.quantity, + categories: this.productDialog.data.categories, + config: { + description: this.productDialog.data.description + } } this.productDialog.showDialog = false if (this.productDialog.data.id) { diff --git a/views_api.py b/views_api.py index 3de3f97..63965d9 100644 --- a/views_api.py +++ b/views_api.py @@ -12,6 +12,7 @@ from lnbits.decorators import ( require_admin_key, require_invoice_key, ) +from lnbits.extensions.nostrmarket.nostr.event import NostrEvent from lnbits.utils.exchange_rates import currencies from . import nostrmarket_ext @@ -24,6 +25,7 @@ from .crud import ( delete_stall, delete_zone, get_merchant_for_user, + get_product, get_products, get_stall, get_stalls, @@ -35,6 +37,7 @@ from .crud import ( ) from .models import ( Merchant, + Nostrable, PartialMerchant, PartialProduct, PartialStall, @@ -266,6 +269,22 @@ async def api_get_stalls(wallet: WalletTypeInfo = Depends(get_key_type)): ) +@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.delete("/api/v1/stall/{stall_id}") async def api_delete_stall( stall_id: str, wallet: WalletTypeInfo = Depends(require_admin_key) @@ -310,6 +329,11 @@ async def api_create_product( data.validate_product() 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( @@ -334,6 +358,11 @@ async def api_update_product( product.validate_product() 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( @@ -348,13 +377,13 @@ async def api_update_product( ) -@nostrmarket_ext.get("/api/v1/product/{stall_id}") +@nostrmarket_ext.get("/api/v1/product/{product_id}") async def api_get_product( - stall_id: str, + product_id: str, wallet: WalletTypeInfo = Depends(require_invoice_key), -): +) -> Optional[Product]: try: - products = await get_products(wallet.wallet.user, stall_id) + products = await get_product(wallet.wallet.user, product_id) return products except Exception as ex: logger.warning(ex) @@ -370,7 +399,18 @@ async def api_delete_product( 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( @@ -385,3 +425,23 @@ async def api_delete_product( @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 From b6ae448beac6d93b39665d760658fd7711e4d871 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Fri, 3 Mar 2023 11:08:55 +0200 Subject: [PATCH 114/891] feat: publish events for products --- crud.py | 12 ++-- migrations.py | 8 +-- models.py | 41 +++++++++-- .../stall-details/stall-details.html | 2 +- .../components/stall-details/stall-details.js | 10 +-- views_api.py | 68 +++++++++++++++++-- 6 files changed, 117 insertions(+), 24 deletions(-) diff --git a/crud.py b/crud.py index 996bfc3..f4d85c5 100644 --- a/crud.py +++ b/crud.py @@ -197,7 +197,7 @@ async def create_product(user_id: str, data: PartialProduct) -> Product: await db.execute( f""" - INSERT INTO nostrmarket.products (user_id, id, stall_id, name, category_list, description, images, price, quantity) + INSERT INTO nostrmarket.products (user_id, id, stall_id, name, images, price, quantity, category_list, meta) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( @@ -205,11 +205,11 @@ async def create_product(user_id: str, data: PartialProduct) -> Product: product_id, data.stall_id, data.name, - json.dumps(data.categories), - data.description, data.image, data.price, data.quantity, + json.dumps(data.categories), + json.dumps(data.config.dict()), ), ) product = await get_product(user_id, product_id) @@ -222,18 +222,18 @@ async def update_product(user_id: str, product: Product) -> Product: await db.execute( f""" - UPDATE nostrmarket.products set name = ?, category_list = ?, description = ?, images = ?, price = ?, quantity = ? + UPDATE nostrmarket.products set name = ?, description = ?, images = ?, price = ?, quantity = ?, category_list = ?, meta = ? WHERE user_id = ? AND id = ? """, ( product.name, - json.dumps(product.categories), - product.description, product.image, product.price, product.quantity, user_id, product.id, + json.dumps(product.categories), + json.dumps(product.config), ), ) updated_product = await get_product(user_id, product.id) diff --git a/migrations.py b/migrations.py index 2e0a158..696e584 100644 --- a/migrations.py +++ b/migrations.py @@ -37,17 +37,17 @@ async def m001_initial(db): Initial products table. """ await db.execute( - f""" + """ CREATE TABLE nostrmarket.products ( user_id TEXT NOT NULL, id TEXT PRIMARY KEY, stall_id TEXT NOT NULL, name TEXT NOT NULL, - category_list TEXT DEFAULT '[]', - description TEXT, images TEXT DEFAULT '[]', price REAL NOT NULL, - quantity INTEGER NOT NULL + quantity INTEGER NOT NULL, + category_list TEXT DEFAULT '[]', + meta TEXT NOT NULL DEFAULT '{}' ); """ ) diff --git a/models.py b/models.py index 412bbfc..0becfe3 100644 --- a/models.py +++ b/models.py @@ -1,5 +1,6 @@ import json import time +from abc import abstractmethod from sqlite3 import Row from typing import List, Optional @@ -8,6 +9,18 @@ from pydantic import BaseModel from .helpers import sign_message_hash from .nostr.event import NostrEvent +######################################## NOSTR ######################################## + + +class Nostrable: + @abstractmethod + def to_nostr_event(self, pubkey: str) -> NostrEvent: + pass + + @abstractmethod + def to_nostr_delete_event(self, pubkey: str) -> NostrEvent: + pass + ######################################## MERCHANT ######################################## class MerchantConfig(BaseModel): @@ -90,7 +103,7 @@ class Stall(PartialStall): event = NostrEvent( pubkey=pubkey, created_at=round(time.time()), - kind=30005, + kind=30017, tags=[["d", self.id]], content=json.dumps(content, separators=(",", ":"), ensure_ascii=False), ) @@ -121,14 +134,19 @@ class Stall(PartialStall): ######################################## STALLS ######################################## +class ProductConfig(BaseModel): + event_id: Optional[str] + description: Optional[str] + + class PartialProduct(BaseModel): stall_id: str name: str categories: List[str] = [] - description: Optional[str] image: Optional[str] price: float quantity: int + config: ProductConfig = ProductConfig() def validate_product(self): if self.image: @@ -150,14 +168,14 @@ class PartialProduct(BaseModel): ) -class Product(PartialProduct): +class Product(PartialProduct, Nostrable): id: str def to_nostr_event(self, pubkey: str) -> NostrEvent: content = { "stall_id": self.stall_id, "name": self.name, - "description": self.description, + "description": self.config.description, "image": self.image, "price": self.price, "quantity": self.quantity, @@ -167,7 +185,7 @@ class Product(PartialProduct): event = NostrEvent( pubkey=pubkey, created_at=round(time.time()), - kind=30005, + kind=30018, tags=[["d", self.id]] + categories, content=json.dumps(content, separators=(",", ":"), ensure_ascii=False), ) @@ -175,8 +193,21 @@ class Product(PartialProduct): return event + def to_nostr_delete_event(self, pubkey: str) -> NostrEvent: + delete_event = NostrEvent( + pubkey=pubkey, + created_at=round(time.time()), + kind=5, + tags=[["e", self.config.event_id]], + content=f"Product '{self.name}' deleted", + ) + delete_event.id = delete_event.event_id + + return delete_event + @classmethod def from_row(cls, row: Row) -> "Product": product = cls(**dict(row)) + product.config = ProductConfig(**json.loads(row["meta"])) product.categories = json.loads(row["category_list"]) return product diff --git a/static/components/stall-details/stall-details.html b/static/components/stall-details/stall-details.html index 82610ca..9246aad 100644 --- a/static/components/stall-details/stall-details.html +++ b/static/components/stall-details/stall-details.html @@ -161,7 +161,7 @@ - {{props.row.description}} + {{props.row.config.description}} diff --git a/static/components/stall-details/stall-details.js b/static/components/stall-details/stall-details.js index e62caaf..b8ef9c8 100644 --- a/static/components/stall-details/stall-details.js +++ b/static/components/stall-details/stall-details.js @@ -190,7 +190,7 @@ async function stallDetails(path) { try { const {data} = await LNbits.api.request( 'GET', - '/nostrmarket/api/v1/product/' + this.stall.id, + '/nostrmarket/api/v1/stall/product/' + this.stall.id, this.inkey ) this.products = data @@ -205,12 +205,14 @@ async function stallDetails(path) { stall_id: this.stall.id, id: this.productDialog.data.id, name: this.productDialog.data.name, - description: this.productDialog.data.description, - categories: this.productDialog.data.categories, image: this.productDialog.data.image, price: this.productDialog.data.price, - quantity: this.productDialog.data.quantity + quantity: this.productDialog.data.quantity, + categories: this.productDialog.data.categories, + config: { + description: this.productDialog.data.description + } } this.productDialog.showDialog = false if (this.productDialog.data.id) { diff --git a/views_api.py b/views_api.py index 3de3f97..63965d9 100644 --- a/views_api.py +++ b/views_api.py @@ -12,6 +12,7 @@ from lnbits.decorators import ( require_admin_key, require_invoice_key, ) +from lnbits.extensions.nostrmarket.nostr.event import NostrEvent from lnbits.utils.exchange_rates import currencies from . import nostrmarket_ext @@ -24,6 +25,7 @@ from .crud import ( delete_stall, delete_zone, get_merchant_for_user, + get_product, get_products, get_stall, get_stalls, @@ -35,6 +37,7 @@ from .crud import ( ) from .models import ( Merchant, + Nostrable, PartialMerchant, PartialProduct, PartialStall, @@ -266,6 +269,22 @@ async def api_get_stalls(wallet: WalletTypeInfo = Depends(get_key_type)): ) +@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.delete("/api/v1/stall/{stall_id}") async def api_delete_stall( stall_id: str, wallet: WalletTypeInfo = Depends(require_admin_key) @@ -310,6 +329,11 @@ async def api_create_product( data.validate_product() 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( @@ -334,6 +358,11 @@ async def api_update_product( product.validate_product() 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( @@ -348,13 +377,13 @@ async def api_update_product( ) -@nostrmarket_ext.get("/api/v1/product/{stall_id}") +@nostrmarket_ext.get("/api/v1/product/{product_id}") async def api_get_product( - stall_id: str, + product_id: str, wallet: WalletTypeInfo = Depends(require_invoice_key), -): +) -> Optional[Product]: try: - products = await get_products(wallet.wallet.user, stall_id) + products = await get_product(wallet.wallet.user, product_id) return products except Exception as ex: logger.warning(ex) @@ -370,7 +399,18 @@ async def api_delete_product( 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( @@ -385,3 +425,23 @@ async def api_delete_product( @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 From 1622403cd187bc66f73fd5e11141dcbfd2e91e32 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Fri, 3 Mar 2023 11:22:48 +0200 Subject: [PATCH 115/891] refactor: stalls use `sign_and_send_to_nostr` --- crud.py | 6 +++--- views_api.py | 30 ++++++++---------------------- 2 files changed, 11 insertions(+), 25 deletions(-) diff --git a/crud.py b/crud.py index f4d85c5..6ad0139 100644 --- a/crud.py +++ b/crud.py @@ -222,7 +222,7 @@ async def update_product(user_id: str, product: Product) -> Product: await db.execute( f""" - UPDATE nostrmarket.products set name = ?, description = ?, images = ?, price = ?, quantity = ?, category_list = ?, meta = ? + UPDATE nostrmarket.products set name = ?, images = ?, price = ?, quantity = ?, category_list = ?, meta = ? WHERE user_id = ? AND id = ? """, ( @@ -230,10 +230,10 @@ async def update_product(user_id: str, product: Product) -> Product: product.image, product.price, product.quantity, + json.dumps(product.categories), + json.dumps(product.config.dict()), user_id, product.id, - json.dumps(product.categories), - json.dumps(product.config), ), ) updated_product = await get_product(user_id, product.id) diff --git a/views_api.py b/views_api.py index 63965d9..e095ee8 100644 --- a/views_api.py +++ b/views_api.py @@ -172,15 +172,9 @@ async def api_create_stall( try: data.validate_stall() - print("### stall", json.dumps(data.dict())) - merchant = await get_merchant_for_user(wallet.wallet.user) - assert merchant, "Cannot find merchat for stall" - stall = await create_stall(wallet.wallet.user, data=data) - event = stall.to_nostr_event(merchant.public_key) - event.sig = merchant.sign_hash(bytes.fromhex(event.id)) - await publish_nostr_event(event) + event = await sign_and_send_to_nostr(wallet.wallet.user, stall) stall.config.event_id = event.id await update_stall(wallet.wallet.user, stall) @@ -207,18 +201,13 @@ async def api_update_stall( try: data.validate_stall() - merchant = await get_merchant_for_user(wallet.wallet.user) - assert merchant, "Cannot find merchat for stall" - - event = data.to_nostr_event(merchant.public_key) - event.sig = merchant.sign_hash(bytes.fromhex(event.id)) - - data.config.event_id = event.id - # data.config.event_created_at = stall = await update_stall(wallet.wallet.user, data) assert stall, "Cannot update stall" - await publish_nostr_event(event) + 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: @@ -297,15 +286,12 @@ async def api_delete_stall( detail="Stall does not exist.", ) - merchant = await get_merchant_for_user(wallet.wallet.user) - assert merchant, "Cannot find merchat for stall" - await delete_stall(wallet.wallet.user, stall_id) - delete_event = stall.to_nostr_delete_event(merchant.public_key) - delete_event.sig = merchant.sign_hash(bytes.fromhex(delete_event.id)) + event = await sign_and_send_to_nostr(wallet.wallet.user, stall, True) - await publish_nostr_event(delete_event) + stall.config.event_id = event.id + await update_stall(wallet.wallet.user, stall) except HTTPException as ex: raise ex From 68927666ee12a4f642c3b4f5bdcda08ee805004f Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Fri, 3 Mar 2023 11:22:48 +0200 Subject: [PATCH 116/891] refactor: stalls use `sign_and_send_to_nostr` --- crud.py | 6 +++--- views_api.py | 30 ++++++++---------------------- 2 files changed, 11 insertions(+), 25 deletions(-) diff --git a/crud.py b/crud.py index f4d85c5..6ad0139 100644 --- a/crud.py +++ b/crud.py @@ -222,7 +222,7 @@ async def update_product(user_id: str, product: Product) -> Product: await db.execute( f""" - UPDATE nostrmarket.products set name = ?, description = ?, images = ?, price = ?, quantity = ?, category_list = ?, meta = ? + UPDATE nostrmarket.products set name = ?, images = ?, price = ?, quantity = ?, category_list = ?, meta = ? WHERE user_id = ? AND id = ? """, ( @@ -230,10 +230,10 @@ async def update_product(user_id: str, product: Product) -> Product: product.image, product.price, product.quantity, + json.dumps(product.categories), + json.dumps(product.config.dict()), user_id, product.id, - json.dumps(product.categories), - json.dumps(product.config), ), ) updated_product = await get_product(user_id, product.id) diff --git a/views_api.py b/views_api.py index 63965d9..e095ee8 100644 --- a/views_api.py +++ b/views_api.py @@ -172,15 +172,9 @@ async def api_create_stall( try: data.validate_stall() - print("### stall", json.dumps(data.dict())) - merchant = await get_merchant_for_user(wallet.wallet.user) - assert merchant, "Cannot find merchat for stall" - stall = await create_stall(wallet.wallet.user, data=data) - event = stall.to_nostr_event(merchant.public_key) - event.sig = merchant.sign_hash(bytes.fromhex(event.id)) - await publish_nostr_event(event) + event = await sign_and_send_to_nostr(wallet.wallet.user, stall) stall.config.event_id = event.id await update_stall(wallet.wallet.user, stall) @@ -207,18 +201,13 @@ async def api_update_stall( try: data.validate_stall() - merchant = await get_merchant_for_user(wallet.wallet.user) - assert merchant, "Cannot find merchat for stall" - - event = data.to_nostr_event(merchant.public_key) - event.sig = merchant.sign_hash(bytes.fromhex(event.id)) - - data.config.event_id = event.id - # data.config.event_created_at = stall = await update_stall(wallet.wallet.user, data) assert stall, "Cannot update stall" - await publish_nostr_event(event) + 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: @@ -297,15 +286,12 @@ async def api_delete_stall( detail="Stall does not exist.", ) - merchant = await get_merchant_for_user(wallet.wallet.user) - assert merchant, "Cannot find merchat for stall" - await delete_stall(wallet.wallet.user, stall_id) - delete_event = stall.to_nostr_delete_event(merchant.public_key) - delete_event.sig = merchant.sign_hash(bytes.fromhex(delete_event.id)) + event = await sign_and_send_to_nostr(wallet.wallet.user, stall, True) - await publish_nostr_event(delete_event) + stall.config.event_id = event.id + await update_stall(wallet.wallet.user, stall) except HTTPException as ex: raise ex From dccd781553fb85d42d57c0882185fb300b6a5257 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Fri, 3 Mar 2023 11:23:09 +0200 Subject: [PATCH 117/891] feat: show stall ID --- models.py | 2 +- static/components/stall-details/stall-details.html | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/models.py b/models.py index 0becfe3..cc82538 100644 --- a/models.py +++ b/models.py @@ -90,7 +90,7 @@ class PartialStall(BaseModel): ) -class Stall(PartialStall): +class Stall(PartialStall, Nostrable): id: str def to_nostr_event(self, pubkey: str) -> NostrEvent: diff --git a/static/components/stall-details/stall-details.html b/static/components/stall-details/stall-details.html index 9246aad..2488726 100644 --- a/static/components/stall-details/stall-details.html +++ b/static/components/stall-details/stall-details.html @@ -7,6 +7,20 @@
+
+
ID:
+
+ +
+
+
Name:
From 2ac7a9f63b72b41bbc2c33fac1aa5ae5e67bdb9b Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Fri, 3 Mar 2023 11:23:09 +0200 Subject: [PATCH 118/891] feat: show stall ID --- models.py | 2 +- static/components/stall-details/stall-details.html | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/models.py b/models.py index 0becfe3..cc82538 100644 --- a/models.py +++ b/models.py @@ -90,7 +90,7 @@ class PartialStall(BaseModel): ) -class Stall(PartialStall): +class Stall(PartialStall, Nostrable): id: str def to_nostr_event(self, pubkey: str) -> NostrEvent: diff --git a/static/components/stall-details/stall-details.html b/static/components/stall-details/stall-details.html index 9246aad..2488726 100644 --- a/static/components/stall-details/stall-details.html +++ b/static/components/stall-details/stall-details.html @@ -7,6 +7,20 @@
+
+
ID:
+
+ +
+
+
Name:
From 39f79fbda545d65051f2c8fd65fc8fa573a077b8 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Fri, 3 Mar 2023 11:30:48 +0200 Subject: [PATCH 119/891] feat: add currency to product --- models.py | 2 ++ nostr/nostr_client.py | 2 +- views_api.py | 13 +++++++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/models.py b/models.py index cc82538..194cf04 100644 --- a/models.py +++ b/models.py @@ -137,6 +137,7 @@ class Stall(PartialStall, Nostrable): class ProductConfig(BaseModel): event_id: Optional[str] description: Optional[str] + currency: Optional[str] class PartialProduct(BaseModel): @@ -177,6 +178,7 @@ class Product(PartialProduct, Nostrable): "name": self.name, "description": self.config.description, "image": self.image, + "currency": self.config.currency, "price": self.price, "quantity": self.quantity, } diff --git a/nostr/nostr_client.py b/nostr/nostr_client.py index 29a5b42..306c123 100644 --- a/nostr/nostr_client.py +++ b/nostr/nostr_client.py @@ -10,7 +10,7 @@ from .event import NostrEvent async def publish_nostr_event(e: NostrEvent): url = url_for("/nostrclient/api/v1/publish", external=True) data = dict(e) - print("### published", dict(data)) + # print("### published", dict(data)) async with httpx.AsyncClient() as client: try: await client.post( diff --git a/views_api.py b/views_api.py index e095ee8..4680977 100644 --- a/views_api.py +++ b/views_api.py @@ -313,6 +313,11 @@ async def api_create_product( ) -> 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) @@ -341,7 +346,15 @@ async def api_update_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) From b9cf0375658f956fa2d65c638bdbbceae3c3fdff Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Fri, 3 Mar 2023 11:30:48 +0200 Subject: [PATCH 120/891] feat: add currency to product --- models.py | 2 ++ nostr/nostr_client.py | 2 +- views_api.py | 13 +++++++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/models.py b/models.py index cc82538..194cf04 100644 --- a/models.py +++ b/models.py @@ -137,6 +137,7 @@ class Stall(PartialStall, Nostrable): class ProductConfig(BaseModel): event_id: Optional[str] description: Optional[str] + currency: Optional[str] class PartialProduct(BaseModel): @@ -177,6 +178,7 @@ class Product(PartialProduct, Nostrable): "name": self.name, "description": self.config.description, "image": self.image, + "currency": self.config.currency, "price": self.price, "quantity": self.quantity, } diff --git a/nostr/nostr_client.py b/nostr/nostr_client.py index 29a5b42..306c123 100644 --- a/nostr/nostr_client.py +++ b/nostr/nostr_client.py @@ -10,7 +10,7 @@ from .event import NostrEvent async def publish_nostr_event(e: NostrEvent): url = url_for("/nostrclient/api/v1/publish", external=True) data = dict(e) - print("### published", dict(data)) + # print("### published", dict(data)) async with httpx.AsyncClient() as client: try: await client.post( diff --git a/views_api.py b/views_api.py index e095ee8..4680977 100644 --- a/views_api.py +++ b/views_api.py @@ -313,6 +313,11 @@ async def api_create_product( ) -> 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) @@ -341,7 +346,15 @@ async def api_update_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) From 6b3cde90a3c9c7e8fdc8b8cdde97a627f670eb40 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Fri, 3 Mar 2023 11:38:11 +0200 Subject: [PATCH 121/891] fix: product description --- .../components/stall-details/stall-details.html | 2 +- static/components/stall-details/stall-details.js | 16 ++++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/static/components/stall-details/stall-details.html b/static/components/stall-details/stall-details.html index 2488726..b26caf3 100644 --- a/static/components/stall-details/stall-details.html +++ b/static/components/stall-details/stall-details.html @@ -201,7 +201,7 @@ Date: Fri, 3 Mar 2023 11:38:11 +0200 Subject: [PATCH 122/891] fix: product description --- .../components/stall-details/stall-details.html | 2 +- static/components/stall-details/stall-details.js | 16 ++++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/static/components/stall-details/stall-details.html b/static/components/stall-details/stall-details.html index 2488726..b26caf3 100644 --- a/static/components/stall-details/stall-details.html +++ b/static/components/stall-details/stall-details.html @@ -201,7 +201,7 @@ Date: Fri, 3 Mar 2023 12:00:49 +0200 Subject: [PATCH 123/891] fix: `product.image` column name --- crud.py | 2 +- migrations.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crud.py b/crud.py index 6ad0139..990b9ad 100644 --- a/crud.py +++ b/crud.py @@ -222,7 +222,7 @@ async def update_product(user_id: str, product: Product) -> Product: await db.execute( f""" - UPDATE nostrmarket.products set name = ?, images = ?, price = ?, quantity = ?, category_list = ?, meta = ? + UPDATE nostrmarket.products set name = ?, image = ?, price = ?, quantity = ?, category_list = ?, meta = ? WHERE user_id = ? AND id = ? """, ( diff --git a/migrations.py b/migrations.py index 696e584..680c3cc 100644 --- a/migrations.py +++ b/migrations.py @@ -43,7 +43,7 @@ async def m001_initial(db): id TEXT PRIMARY KEY, stall_id TEXT NOT NULL, name TEXT NOT NULL, - images TEXT DEFAULT '[]', + image TEXT DEFAULT, price REAL NOT NULL, quantity INTEGER NOT NULL, category_list TEXT DEFAULT '[]', From 9c410d686a0ea592947e433598d1921689442da0 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Fri, 3 Mar 2023 12:00:49 +0200 Subject: [PATCH 124/891] fix: `product.image` column name --- crud.py | 2 +- migrations.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crud.py b/crud.py index 6ad0139..990b9ad 100644 --- a/crud.py +++ b/crud.py @@ -222,7 +222,7 @@ async def update_product(user_id: str, product: Product) -> Product: await db.execute( f""" - UPDATE nostrmarket.products set name = ?, images = ?, price = ?, quantity = ?, category_list = ?, meta = ? + UPDATE nostrmarket.products set name = ?, image = ?, price = ?, quantity = ?, category_list = ?, meta = ? WHERE user_id = ? AND id = ? """, ( diff --git a/migrations.py b/migrations.py index 696e584..680c3cc 100644 --- a/migrations.py +++ b/migrations.py @@ -43,7 +43,7 @@ async def m001_initial(db): id TEXT PRIMARY KEY, stall_id TEXT NOT NULL, name TEXT NOT NULL, - images TEXT DEFAULT '[]', + image TEXT DEFAULT, price REAL NOT NULL, quantity INTEGER NOT NULL, category_list TEXT DEFAULT '[]', From dc1650d036920b52bed8c9764a6ac7237d97dfc7 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Fri, 3 Mar 2023 10:24:42 +0000 Subject: [PATCH 125/891] mergeable-ish client UI --- .../customer-market/customer-market.html | 12 +- .../customer-stall/customer-stall.html | 1 + .../customer-stall/customer-stall.js | 21 +- .../product-detail/product-detail.html | 188 +++++++++++++++++- .../product-detail/product-detail.js | 17 +- templates/nostrmarket/market.html | 108 +++++----- 6 files changed, 283 insertions(+), 64 deletions(-) diff --git a/static/components/customer-market/customer-market.html b/static/components/customer-market/customer-market.html index b52c6c2..97a00b9 100644 --- a/static/components/customer-market/customer-market.html +++ b/static/components/customer-market/customer-market.html @@ -70,13 +70,21 @@ Stall: {{ item.stallName }}
- See product + View details + + Visit Stall diff --git a/static/components/customer-stall/customer-stall.html b/static/components/customer-stall/customer-stall.html index c99290e..306a5f7 100644 --- a/static/components/customer-stall/customer-stall.html +++ b/static/components/customer-stall/customer-stall.html @@ -12,6 +12,7 @@ +
+
+
+
+ + + +
+
+
+
+
+
{{ product.name }}
+
+ {{cat}} +
+
{{ product.description }}
+
+ + {{ product.price }} satsBTC {{ (product.price / 1e8).toFixed(8) }} + + + {{ product.formatedPrice }} + ({{ product.priceInSats }} sats) + + {{ product.amount > 0 ? 'In stock.' : 'Out of stock.' }} + +
+
+ + +
+
+ +
+
Customer rating
+
4.2
+
+ +
+
(357 reviews)
+
+ 93% would recommend to a friend +
+ + + + 5 + + + 273 + + + 4 + + +   69 + + + 3 + + +      6 + + + 2 + + +      3 + + + 1 + + +      6 + + +
+
+
+
+ +
+
diff --git a/static/components/product-detail/product-detail.js b/static/components/product-detail/product-detail.js index 27a8d72..7b60f6b 100644 --- a/static/components/product-detail/product-detail.js +++ b/static/components/product-detail/product-detail.js @@ -6,8 +6,21 @@ async function productDetail(path) { props: ['product'], data: function () { - return {} + return { + slide: 1 + } }, - methods: {} + computed: { + win_width() { + return this.$q.screen.width - 59 + }, + win_height() { + return this.$q.screen.height - 0 + } + }, + methods: {}, + created() { + console.log('ping') + } }) } diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html index dc835cb..8469175 100644 --- a/templates/nostrmarket/market.html +++ b/templates/nostrmarket/market.html @@ -137,35 +137,6 @@ -
@@ -174,6 +145,7 @@ :stall="stalls.find(stall => stall.id == activeStall)" :products="filterProducts" :exchange-rates="exchangeRates" + :product-detail="activeProduct" @change-page="navigateTo" > + + + - - Vue.component(VueQrcode.name, VueQrcode) - - Promise.all([ - customerMarket('static/components/customer-market/customer-market.html'), - customerStall('static/components/customer-stall/customer-stall.html'), - productDetail('static/components/product-detail/product-detail.html') - ]) - - new Vue({ - el: '#vue', - mixins: [windowMixin], - data: function () { - return { - drawer: false, - pubkeys: new Set(), - relays: new Set(), - events: [], - stalls: [], - products: [], - profiles: new Map(), - searchText: null, - exchangeRates: null, - inputPubkey: null, - inputRelay: null, - activePage: 'market', - activeStall: null, - activeProduct: null - } - }, - computed: { - filterProducts() { - let products = this.products - if (this.activeStall) { - products = products.filter(p => p.stall == this.activeStall) - } - if (!this.searchText || this.searchText.length < 2) return products - return products.filter(p => { - return ( - p.name.includes(this.searchText) || - p.description.includes(this.searchText) || - p.categories.includes(this.searchText) - ) - }) - }, - stallName() { - return this.stalls.find(s => s.id == this.activeStall)?.name || 'Stall' - }, - productName() { - return ( - this.products.find(p => p.id == this.activeProduct)?.name || 'Product' - ) - } - }, - async created() { - // Check for stored merchants and relays on localStorage - try { - let merchants = this.$q.localStorage.getItem(`diagonAlley.merchants`) - let relays = this.$q.localStorage.getItem(`diagonAlley.relays`) - if (merchants && merchants.length) { - this.pubkeys = new Set(merchants) - } - if (relays && relays.length) { - this.relays = new Set([...defaultRelays, ...relays]) - } - } catch (e) { - console.error(e) - } - // Hardcode pubkeys for testing - /* - this.pubkeys.add( - 'c1415f950a1e3431de2bc5ee35144639e2f514cf158279abff9ed77d50118796' - ) - this.pubkeys.add( - '8f69ac99b96f7c4ad58b98cc38fe5d35ce02daefae7d1609c797ce3b4f92f5fd' - ) - */ - // stall ids S4hQgtTwiF5kGJZPbqMH9M jkCbdtkXeMjGBY3LBf8yn4 - let merchant_pubkey = JSON.parse('{{ merchant_pubkey | tojson }}') - let stall_id = JSON.parse('{{ stall_id | tojson }}') - let product_id = JSON.parse('{{ product_id | tojson }}') - if (merchant_pubkey) { - await addPubkey(merchant_pubkey) - /*LNbits.utils - .confirmDialog( - `We found a merchant pubkey in your request. Do you want to add it to the merchants list?` - ) - .onCancel(() => {}) - .onDismiss(() => {}) - .onOk(() => { - this.pubkeys.add(merchant_pubkey) - })*/ - } - this.$q.loading.show() - this.relays = new Set(defaultRelays) - // Get notes from Nostr - await this.initNostr() - - // What component to render on start - if (stall_id) { - if (product_id) { - this.activeProduct = product_id - } - this.activePage = 'stall' - this.activeStall = stall_id - } - - this.$q.loading.hide() - }, - methods: { - async initNostr() { - const pool = new nostr.SimplePool() - let relays = Array.from(this.relays) - let products = new Map() - let stalls = new Map() - // Get metadata and market data from the pubkeys - let sub = await pool - .list(relays, [ - { - kinds: [0, 30017, 30018], // for production kind is 30017 - authors: Array.from(this.pubkeys) - } - ]) - .then(events => { - console.log(events) - this.events = events || [] - this.events.map(eventToObj).map(e => { - if (e.kind == 0) { - this.profiles.set(e.pubkey, e.content) - return - } else if (e.kind == 30018) { - //it's a product `d` is the prod. id - products.set(e.d, {...e.content, id: e.d, categories: e.t}) - } else if (e.kind == 30017) { - // it's a stall `d` is the stall id - stalls.set(e.d, {...e.content, id: e.d, pubkey: e.pubkey}) - return - } - }) - }) - await Promise.resolve(sub) - this.stalls = await Array.from(stalls.values()) - - this.products = Array.from(products.values()).map(obj => { - let stall = this.stalls.find(s => s.id == obj.stall_id) - obj.stallName = stall.name - if (obj.currency != 'sat') { - obj.formatedPrice = this.getAmountFormated(obj.price, obj.currency) - obj.priceInSats = this.getValueInSats(obj.price, obj.currency) - } - return obj - }) - - pool.close(relays) - }, - async getRates() { - let noFiat = this.stalls.map(s => s.currency).every(c => c == 'sat') - if (noFiat) return - try { - let rates = await axios.get('https://api.opennode.co/v1/rates') - this.exchangeRates = rates.data.data - } catch (error) { - LNbits.utils.notifyApiError(error) - } - }, - navigateTo(page, opts = {stall: null, product: null, pubkey: null}) { - let {stall, product, pubkey} = opts - let url = new URL(window.location) - - if (pubkey) url.searchParams.set('merchant_pubkey', pubkey) - if (stall && !pubkey) { - pubkey = this.stalls.find(s => s.id == stall).pubkey - url.searchParams.set('merchant_pubkey', pubkey) - } - - switch (page) { - case 'stall': - if (stall) { - this.activeStall = stall - url.searchParams.set('stall_id', stall) - if (product) { - this.activeProduct = product - url.searchParams.set('product_id', product) - } - } - break - default: - this.activeStall = null - this.activeProduct = null - url.searchParams.delete('merchant_pubkey') - url.searchParams.delete('stall_id') - url.searchParams.delete('product_id') - break - } - - window.history.pushState({}, '', url) - this.activePage = page - }, - - getValueInSats(amount, unit = 'USD') { - if (!this.exchangeRates) return 0 - return Math.ceil( - (amount / this.exchangeRates[`BTC${unit}`][unit]) * 1e8 - ) - }, - getAmountFormated(amount, unit = 'USD') { - return LNbits.utils.formatCurrency(amount, unit) - }, - async addPubkey(pubkey = null) { - if (!pubkey) { - pubkey = String(this.inputPubkey).trim() - } - let regExp = /^#([0-9a-f]{3}){1,2}$/i - if (pubkey.startsWith('n')) { - try { - let {type, data} = nostr.nip19.decode(pubkey) - if (type === 'npub') pubkey = data - else if (type === 'nprofile') { - pubkey = data.pubkey - givenRelays = data.relays - } - this.pubkeys.add(pubkey) - this.inputPubkey = null - } catch (err) { - console.error(err) - } - } else if (regExp.test(pubkey)) { - pubkey = pubkey - } - this.pubkeys.add(pubkey) - this.$q.localStorage.set( - `diagonAlley.merchants`, - Array.from(this.pubkeys) - ) - await this.initNostr() - }, - removePubkey(pubkey) { - // Needs a hack for Vue reactivity - let pubkeys = this.pubkeys - pubkeys.delete(pubkey) - this.profiles.delete(pubkey) - this.pubkeys = new Set(Array.from(pubkeys)) - this.$q.localStorage.set( - `diagonAlley.merchants`, - Array.from(this.pubkeys) - ) - }, - async addRelay() { - let relay = String(this.inputRelay).trim() - if (!relay.startsWith('ws')) { - console.debug('invalid url') - return - } - this.relays.add(relay) - this.$q.localStorage.set(`diagonAlley.relays`, Array.from(this.relays)) - this.inputRelay = null - await this.initNostr() - }, - removeRelay(relay) { - // Needs a hack for Vue reactivity - let relays = this.relays - relays.delete(relay) - this.relays = new Set(Array.from(relays)) - this.$q.localStorage.set(`diagonAlley.relays`, Array.from(this.relays)) - } - } - }) - {% endblock %} From eaa432e628bd44babc452410d7cef840718cfb2a Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Fri, 3 Mar 2023 14:09:13 +0000 Subject: [PATCH 128/891] detach js from html --- .../customer-market/customer-market.html | 82 +---- .../customer-market/customer-market.js | 9 +- .../components/product-card/product-card.html | 76 +++++ .../components/product-card/product-card.js | 14 + static/js/market.js | 308 ++++++++++++++++++ templates/nostrmarket/market.html | 296 +---------------- 6 files changed, 404 insertions(+), 381 deletions(-) create mode 100644 static/components/product-card/product-card.html create mode 100644 static/components/product-card/product-card.js create mode 100644 static/js/market.js diff --git a/static/components/customer-market/customer-market.html b/static/components/customer-market/customer-market.html index 97a00b9..5de0b0d 100644 --- a/static/components/customer-market/customer-market.html +++ b/static/components/customer-market/customer-market.html @@ -10,87 +10,7 @@ v-for="(item, idx) in products" :key="idx" > - - - - -
-
- {{ item.name }} -
-
- - -
- - -
-
- {{ item.stallName }} -
- - {{ item.price }} satsBTC {{ (item.price / 1e8).toFixed(8) }} - - - {{ item.formatedPrice }} - ({{ item.priceInSats }} sats) - - {{ item.amount }} left -
-
- {{cat}} -
-
-

{{ item.description }}

-
-
- - - - - Stall: {{ item.stallName }} -
- - View details - - - Visit Stall - -
-
-
+
diff --git a/static/components/customer-market/customer-market.js b/static/components/customer-market/customer-market.js index 5844105..992222a 100644 --- a/static/components/customer-market/customer-market.js +++ b/static/components/customer-market/customer-market.js @@ -4,14 +4,11 @@ async function customerMarket(path) { name: 'customer-market', template, - props: ['products', 'exchange-rates'], + props: ['products', 'exchange-rates', 'change-page'], data: function () { return {} }, - methods: { - changePage() { - return - } - } + methods: {}, + created() {} }) } diff --git a/static/components/product-card/product-card.html b/static/components/product-card/product-card.html new file mode 100644 index 0000000..5ff2782 --- /dev/null +++ b/static/components/product-card/product-card.html @@ -0,0 +1,76 @@ + + + + +
+
{{ product.name }}
+
+ + +
+ + +
+
{{ product.stallName }}
+ + {{ product.price }} satsBTC {{ (product.price / 1e8).toFixed(8) }} + + + {{ product.formatedPrice }} + ({{ product.priceInSats }} sats) + + {{ product.quantity }} left +
+
+ {{cat}} +
+
+

{{ product.description }}

+
+
+ + + + + Stall: {{ product.stallName }} +
+ + View details + + + Visit Stall + +
+
+
diff --git a/static/components/product-card/product-card.js b/static/components/product-card/product-card.js new file mode 100644 index 0000000..da3fca3 --- /dev/null +++ b/static/components/product-card/product-card.js @@ -0,0 +1,14 @@ +async function productCard(path) { + const template = await loadTemplateAsync(path) + Vue.component('product-card', { + name: 'product-card', + template, + + props: ['product'], + data: function () { + return {} + }, + methods: {}, + created() {} + }) +} diff --git a/static/js/market.js b/static/js/market.js new file mode 100644 index 0000000..3c823fc --- /dev/null +++ b/static/js/market.js @@ -0,0 +1,308 @@ +const market = async () => { + Vue.component(VueQrcode.name, VueQrcode) + + const nostr = window.NostrTools + const defaultRelays = [ + 'wss://relay.damus.io', + 'wss://relay.snort.social', + 'wss://nos.lol', + 'wss://nostr.wine', + 'wss://relay.nostr.bg', + 'wss://nostr-pub.wellorder.net', + 'wss://nostr-pub.semisol.dev', + 'wss://eden.nostr.land', + 'wss://nostr.mom', + 'wss://nostr.fmt.wiz.biz', + 'wss://nostr.zebedee.cloud', + 'wss://nostr.rocks' + ] + const eventToObj = event => { + event.content = JSON.parse(event.content) + return { + ...event, + ...Object.values(event.tags).reduce((acc, tag) => { + let [key, value] = tag + return {...acc, [key]: [...(acc[key] || []), value]} + }, {}) + } + } + await Promise.all([ + productCard('static/components/product-card/product-card.html'), + customerMarket('static/components/customer-market/customer-market.html'), + customerStall('static/components/customer-stall/customer-stall.html'), + productDetail('static/components/product-detail/product-detail.html') + ]) + + new Vue({ + el: '#vue', + mixins: [windowMixin], + data: function () { + return { + drawer: false, + pubkeys: new Set(), + relays: new Set(), + events: [], + stalls: [], + products: [], + profiles: new Map(), + searchText: null, + exchangeRates: null, + inputPubkey: null, + inputRelay: null, + activePage: 'market', + activeStall: null, + activeProduct: null + } + }, + computed: { + filterProducts() { + let products = this.products + if (this.activeStall) { + products = products.filter(p => p.stall == this.activeStall) + } + if (!this.searchText || this.searchText.length < 2) return products + return products.filter(p => { + return ( + p.name.includes(this.searchText) || + p.description.includes(this.searchText) || + p.categories.includes(this.searchText) + ) + }) + }, + stallName() { + return this.stalls.find(s => s.id == this.activeStall)?.name || 'Stall' + }, + productName() { + return ( + this.products.find(p => p.id == this.activeProduct)?.name || 'Product' + ) + } + }, + async created() { + // Check for stored merchants and relays on localStorage + try { + let merchants = this.$q.localStorage.getItem(`diagonAlley.merchants`) + let relays = this.$q.localStorage.getItem(`diagonAlley.relays`) + if (merchants && merchants.length) { + this.pubkeys = new Set(merchants) + } + if (relays && relays.length) { + this.relays = new Set([...defaultRelays, ...relays]) + } + } catch (e) { + console.error(e) + } + // Hardcode pubkeys for testing + /* + this.pubkeys.add( + 'c1415f950a1e3431de2bc5ee35144639e2f514cf158279abff9ed77d50118796' + ) + this.pubkeys.add( + '8f69ac99b96f7c4ad58b98cc38fe5d35ce02daefae7d1609c797ce3b4f92f5fd' + ) + */ + // stall ids S4hQgtTwiF5kGJZPbqMH9M jkCbdtkXeMjGBY3LBf8yn4 + /*let naddr = nostr.nip19.naddrEncode({ + identifier: '1234', + pubkey: + 'c1415f950a1e3431de2bc5ee35144639e2f514cf158279abff9ed77d50118796', + kind: 30018, + relays: defaultRelays + }) + console.log(naddr) + console.log(nostr.nip19.decode(naddr)) + */ + let params = new URLSearchParams(window.location.search) + let merchant_pubkey = params.get('merchant_pubkey') + let stall_id = params.get('stall_id') + let product_id = params.get('product_id') + console.log(merchant_pubkey, stall_id, product_id) + if (merchant_pubkey) { + await addPubkey(merchant_pubkey) + /*LNbits.utils + .confirmDialog( + `We found a merchant pubkey in your request. Do you want to add it to the merchants list?` + ) + .onCancel(() => {}) + .onDismiss(() => {}) + .onOk(() => { + this.pubkeys.add(merchant_pubkey) + })*/ + } + this.$q.loading.show() + this.relays = new Set(defaultRelays) + // Get notes from Nostr + await this.initNostr() + + // What component to render on start + if (stall_id) { + if (product_id) { + this.activeProduct = product_id + } + this.activePage = 'stall' + this.activeStall = stall_id + } + + this.$q.loading.hide() + }, + methods: { + async initNostr() { + const pool = new nostr.SimplePool() + let relays = Array.from(this.relays) + let products = new Map() + let stalls = new Map() + // Get metadata and market data from the pubkeys + let sub = await pool + .list(relays, [ + { + kinds: [0, 30017, 30018], // for production kind is 30017 + authors: Array.from(this.pubkeys) + } + ]) + .then(events => { + console.log(events) + this.events = events || [] + this.events.map(eventToObj).map(e => { + if (e.kind == 0) { + this.profiles.set(e.pubkey, e.content) + return + } else if (e.kind == 30018) { + //it's a product `d` is the prod. id + products.set(e.d, {...e.content, id: e.d, categories: e.t}) + } else if (e.kind == 30017) { + // it's a stall `d` is the stall id + stalls.set(e.d, {...e.content, id: e.d, pubkey: e.pubkey}) + return + } + }) + }) + await Promise.resolve(sub) + this.stalls = await Array.from(stalls.values()) + + this.products = Array.from(products.values()).map(obj => { + let stall = this.stalls.find(s => s.id == obj.stall_id) + obj.stallName = stall.name + if (obj.currency != 'sat') { + obj.formatedPrice = this.getAmountFormated(obj.price, obj.currency) + obj.priceInSats = this.getValueInSats(obj.price, obj.currency) + } + return obj + }) + + pool.close(relays) + }, + async getRates() { + let noFiat = this.stalls.map(s => s.currency).every(c => c == 'sat') + if (noFiat) return + try { + let rates = await axios.get('https://api.opennode.co/v1/rates') + this.exchangeRates = rates.data.data + } catch (error) { + LNbits.utils.notifyApiError(error) + } + }, + navigateTo(page, opts = {stall: null, product: null, pubkey: null}) { + let {stall, product, pubkey} = opts + let url = new URL(window.location) + + if (pubkey) url.searchParams.set('merchant_pubkey', pubkey) + if (stall && !pubkey) { + pubkey = this.stalls.find(s => s.id == stall).pubkey + url.searchParams.set('merchant_pubkey', pubkey) + } + + switch (page) { + case 'stall': + if (stall) { + this.activeStall = stall + url.searchParams.set('stall_id', stall) + if (product) { + this.activeProduct = product + url.searchParams.set('product_id', product) + } + } + break + default: + this.activeStall = null + this.activeProduct = null + url.searchParams.delete('merchant_pubkey') + url.searchParams.delete('stall_id') + url.searchParams.delete('product_id') + break + } + + window.history.pushState({}, '', url) + this.activePage = page + }, + + getValueInSats(amount, unit = 'USD') { + if (!this.exchangeRates) return 0 + return Math.ceil( + (amount / this.exchangeRates[`BTC${unit}`][unit]) * 1e8 + ) + }, + getAmountFormated(amount, unit = 'USD') { + return LNbits.utils.formatCurrency(amount, unit) + }, + async addPubkey(pubkey = null) { + if (!pubkey) { + pubkey = String(this.inputPubkey).trim() + } + let regExp = /^#([0-9a-f]{3}){1,2}$/i + if (pubkey.startsWith('n')) { + try { + let {type, data} = nostr.nip19.decode(pubkey) + if (type === 'npub') pubkey = data + else if (type === 'nprofile') { + pubkey = data.pubkey + givenRelays = data.relays + } + this.pubkeys.add(pubkey) + this.inputPubkey = null + } catch (err) { + console.error(err) + } + } else if (regExp.test(pubkey)) { + pubkey = pubkey + } + this.pubkeys.add(pubkey) + this.$q.localStorage.set( + `diagonAlley.merchants`, + Array.from(this.pubkeys) + ) + await this.initNostr() + }, + removePubkey(pubkey) { + // Needs a hack for Vue reactivity + let pubkeys = this.pubkeys + pubkeys.delete(pubkey) + this.profiles.delete(pubkey) + this.pubkeys = new Set(Array.from(pubkeys)) + this.$q.localStorage.set( + `diagonAlley.merchants`, + Array.from(this.pubkeys) + ) + }, + async addRelay() { + let relay = String(this.inputRelay).trim() + if (!relay.startsWith('ws')) { + console.debug('invalid url') + return + } + this.relays.add(relay) + this.$q.localStorage.set(`diagonAlley.relays`, Array.from(this.relays)) + this.inputRelay = null + await this.initNostr() + }, + removeRelay(relay) { + // Needs a hack for Vue reactivity + let relays = this.relays + relays.delete(relay) + this.relays = new Set(Array.from(relays)) + this.$q.localStorage.set(`diagonAlley.relays`, Array.from(this.relays)) + } + } + }) +} + +market() diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html index 8469175..0d3c36e 100644 --- a/templates/nostrmarket/market.html +++ b/templates/nostrmarket/market.html @@ -139,7 +139,6 @@
- + - - Vue.component(VueQrcode.name, VueQrcode) - - Promise.all([ - customerMarket('static/components/customer-market/customer-market.html'), - customerStall('static/components/customer-stall/customer-stall.html'), - productDetail('static/components/product-detail/product-detail.html') - ]) - - new Vue({ - el: '#vue', - mixins: [windowMixin], - data: function () { - return { - drawer: false, - pubkeys: new Set(), - relays: new Set(), - events: [], - stalls: [], - products: [], - profiles: new Map(), - searchText: null, - exchangeRates: null, - inputPubkey: null, - inputRelay: null, - activePage: 'market', - activeStall: null, - activeProduct: null - } - }, - computed: { - filterProducts() { - let products = this.products - if (this.activeStall) { - products = products.filter(p => p.stall == this.activeStall) - } - if (!this.searchText || this.searchText.length < 2) return products - return products.filter(p => { - return ( - p.name.includes(this.searchText) || - p.description.includes(this.searchText) || - p.categories.includes(this.searchText) - ) - }) - }, - stallName() { - return this.stalls.find(s => s.id == this.activeStall)?.name || 'Stall' - }, - productName() { - return ( - this.products.find(p => p.id == this.activeProduct)?.name || 'Product' - ) - } - }, - async created() { - // Check for stored merchants and relays on localStorage - try { - let merchants = this.$q.localStorage.getItem(`diagonAlley.merchants`) - let relays = this.$q.localStorage.getItem(`diagonAlley.relays`) - if (merchants && merchants.length) { - this.pubkeys = new Set(merchants) - } - if (relays && relays.length) { - this.relays = new Set([...defaultRelays, ...relays]) - } - } catch (e) { - console.error(e) - } - // Hardcode pubkeys for testing - /* - this.pubkeys.add( - 'c1415f950a1e3431de2bc5ee35144639e2f514cf158279abff9ed77d50118796' - ) - this.pubkeys.add( - '8f69ac99b96f7c4ad58b98cc38fe5d35ce02daefae7d1609c797ce3b4f92f5fd' - ) - */ - // stall ids S4hQgtTwiF5kGJZPbqMH9M jkCbdtkXeMjGBY3LBf8yn4 - let merchant_pubkey = JSON.parse('{{ merchant_pubkey | tojson }}') - let stall_id = JSON.parse('{{ stall_id | tojson }}') - let product_id = JSON.parse('{{ product_id | tojson }}') - if (merchant_pubkey) { - await addPubkey(merchant_pubkey) - /*LNbits.utils - .confirmDialog( - `We found a merchant pubkey in your request. Do you want to add it to the merchants list?` - ) - .onCancel(() => {}) - .onDismiss(() => {}) - .onOk(() => { - this.pubkeys.add(merchant_pubkey) - })*/ - } - this.$q.loading.show() - this.relays = new Set(defaultRelays) - // Get notes from Nostr - await this.initNostr() - - // What component to render on start - if (stall_id) { - if (product_id) { - this.activeProduct = product_id - } - this.activePage = 'stall' - this.activeStall = stall_id - } - - this.$q.loading.hide() - }, - methods: { - async initNostr() { - const pool = new nostr.SimplePool() - let relays = Array.from(this.relays) - let products = new Map() - let stalls = new Map() - // Get metadata and market data from the pubkeys - let sub = await pool - .list(relays, [ - { - kinds: [0, 30017, 30018], // for production kind is 30017 - authors: Array.from(this.pubkeys) - } - ]) - .then(events => { - console.log(events) - this.events = events || [] - this.events.map(eventToObj).map(e => { - if (e.kind == 0) { - this.profiles.set(e.pubkey, e.content) - return - } else if (e.kind == 30018) { - //it's a product `d` is the prod. id - products.set(e.d, {...e.content, id: e.d, categories: e.t}) - } else if (e.kind == 30017) { - // it's a stall `d` is the stall id - stalls.set(e.d, {...e.content, id: e.d, pubkey: e.pubkey}) - return - } - }) - }) - await Promise.resolve(sub) - this.stalls = await Array.from(stalls.values()) - - this.products = Array.from(products.values()).map(obj => { - let stall = this.stalls.find(s => s.id == obj.stall_id) - obj.stallName = stall.name - if (obj.currency != 'sat') { - obj.formatedPrice = this.getAmountFormated(obj.price, obj.currency) - obj.priceInSats = this.getValueInSats(obj.price, obj.currency) - } - return obj - }) - - pool.close(relays) - }, - async getRates() { - let noFiat = this.stalls.map(s => s.currency).every(c => c == 'sat') - if (noFiat) return - try { - let rates = await axios.get('https://api.opennode.co/v1/rates') - this.exchangeRates = rates.data.data - } catch (error) { - LNbits.utils.notifyApiError(error) - } - }, - navigateTo(page, opts = {stall: null, product: null, pubkey: null}) { - let {stall, product, pubkey} = opts - let url = new URL(window.location) - - if (pubkey) url.searchParams.set('merchant_pubkey', pubkey) - if (stall && !pubkey) { - pubkey = this.stalls.find(s => s.id == stall).pubkey - url.searchParams.set('merchant_pubkey', pubkey) - } - - switch (page) { - case 'stall': - if (stall) { - this.activeStall = stall - url.searchParams.set('stall_id', stall) - if (product) { - this.activeProduct = product - url.searchParams.set('product_id', product) - } - } - break - default: - this.activeStall = null - this.activeProduct = null - url.searchParams.delete('merchant_pubkey') - url.searchParams.delete('stall_id') - url.searchParams.delete('product_id') - break - } - - window.history.pushState({}, '', url) - this.activePage = page - }, - - getValueInSats(amount, unit = 'USD') { - if (!this.exchangeRates) return 0 - return Math.ceil( - (amount / this.exchangeRates[`BTC${unit}`][unit]) * 1e8 - ) - }, - getAmountFormated(amount, unit = 'USD') { - return LNbits.utils.formatCurrency(amount, unit) - }, - async addPubkey(pubkey = null) { - if (!pubkey) { - pubkey = String(this.inputPubkey).trim() - } - let regExp = /^#([0-9a-f]{3}){1,2}$/i - if (pubkey.startsWith('n')) { - try { - let {type, data} = nostr.nip19.decode(pubkey) - if (type === 'npub') pubkey = data - else if (type === 'nprofile') { - pubkey = data.pubkey - givenRelays = data.relays - } - this.pubkeys.add(pubkey) - this.inputPubkey = null - } catch (err) { - console.error(err) - } - } else if (regExp.test(pubkey)) { - pubkey = pubkey - } - this.pubkeys.add(pubkey) - this.$q.localStorage.set( - `diagonAlley.merchants`, - Array.from(this.pubkeys) - ) - await this.initNostr() - }, - removePubkey(pubkey) { - // Needs a hack for Vue reactivity - let pubkeys = this.pubkeys - pubkeys.delete(pubkey) - this.profiles.delete(pubkey) - this.pubkeys = new Set(Array.from(pubkeys)) - this.$q.localStorage.set( - `diagonAlley.merchants`, - Array.from(this.pubkeys) - ) - }, - async addRelay() { - let relay = String(this.inputRelay).trim() - if (!relay.startsWith('ws')) { - console.debug('invalid url') - return - } - this.relays.add(relay) - this.$q.localStorage.set(`diagonAlley.relays`, Array.from(this.relays)) - this.inputRelay = null - await this.initNostr() - }, - removeRelay(relay) { - // Needs a hack for Vue reactivity - let relays = this.relays - relays.delete(relay) - this.relays = new Set(Array.from(relays)) - this.$q.localStorage.set(`diagonAlley.relays`, Array.from(this.relays)) - } - } - }) - {% endblock %} From cec7d2ee254aeaec155c8bf4e3ff9da3e2ee1aa3 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Fri, 3 Mar 2023 18:24:53 +0200 Subject: [PATCH 129/891] feat: listen for direct messages --- __init__.py | 10 +++--- crud.py | 17 +++++++++++ models.py | 6 +++- nostr/nostr_client.py | 47 +++++++++++++++++++++++++++- tasks.py | 71 ++++++++++++++++++++++++++++++++++++++++--- views_api.py | 2 +- 6 files changed, 142 insertions(+), 11 deletions(-) diff --git a/__init__.py b/__init__.py index 9e188f2..9f3a39e 100644 --- a/__init__.py +++ b/__init__.py @@ -1,4 +1,5 @@ import asyncio +from asyncio import Task from typing import List from fastapi import APIRouter @@ -25,9 +26,9 @@ def nostrmarket_renderer(): return template_renderer(["lnbits/extensions/nostrmarket/templates"]) -scheduled_tasks: List[asyncio.Task] = [] +scheduled_tasks: List[Task] = [] -from .tasks import subscribe_nostrclient_ws, wait_for_paid_invoices +from .tasks import subscribe_nostrclient, wait_for_nostr_events, wait_for_paid_invoices from .views import * # noqa from .views_api import * # noqa @@ -35,5 +36,6 @@ from .views_api import * # noqa def nostrmarket_start(): loop = asyncio.get_event_loop() task1 = loop.create_task(catch_everything_and_restart(wait_for_paid_invoices)) - task2 = loop.create_task(catch_everything_and_restart(subscribe_nostrclient_ws)) - scheduled_tasks.append([task1, task2]) + task2 = loop.create_task(catch_everything_and_restart(subscribe_nostrclient)) + task3 = loop.create_task(catch_everything_and_restart(wait_for_nostr_events)) + scheduled_tasks.append([task1, task2, task3]) diff --git a/crud.py b/crud.py index 990b9ad..edd9a09 100644 --- a/crud.py +++ b/crud.py @@ -45,6 +45,23 @@ async def get_merchant(user_id: str, merchant_id: str) -> Optional[Merchant]: return Merchant.from_row(row) if row else None +async def get_merchant_by_pubkey(public_key: str) -> Optional[Merchant]: + row = await db.fetchone( + """SELECT * FROM nostrmarket.merchants WHERE public_key = ? """, + (public_key,), + ) + + return Merchant.from_row(row) if row else None + + +async def get_public_keys_for_merchants() -> List[str]: + rows = await db.fetchall( + """SELECT public_key FROM nostrmarket.merchants""", + ) + + return [row[0] for row in rows] + + async def get_merchant_for_user(user_id: str) -> Optional[Merchant]: row = await db.fetchone( """SELECT * FROM nostrmarket.merchants WHERE user_id = ? """, diff --git a/models.py b/models.py index 194cf04..58b093d 100644 --- a/models.py +++ b/models.py @@ -6,7 +6,7 @@ from typing import List, Optional from pydantic import BaseModel -from .helpers import sign_message_hash +from .helpers import decrypt_message, get_shared_secret, sign_message_hash from .nostr.event import NostrEvent ######################################## NOSTR ######################################## @@ -39,6 +39,10 @@ class Merchant(PartialMerchant): def sign_hash(self, hash: bytes) -> str: return sign_message_hash(self.private_key, hash) + def decrypt_message(self, encrypted_message: str, public_key: str) -> str: + encryption_key = get_shared_secret(self.private_key, public_key) + return decrypt_message(encrypted_message, encryption_key) + @classmethod def from_row(cls, row: Row) -> "Merchant": merchant = cls(**dict(row)) diff --git a/nostr/nostr_client.py b/nostr/nostr_client.py index 306c123..bb64c58 100644 --- a/nostr/nostr_client.py +++ b/nostr/nostr_client.py @@ -1,5 +1,9 @@ +from threading import Thread +from typing import Callable + import httpx from loguru import logger +from websocket import WebSocketApp from lnbits.app import settings from lnbits.helpers import url_for @@ -10,7 +14,7 @@ from .event import NostrEvent async def publish_nostr_event(e: NostrEvent): url = url_for("/nostrclient/api/v1/publish", external=True) data = dict(e) - # print("### published", dict(data)) + print("### published", dict(data)) async with httpx.AsyncClient() as client: try: await client.post( @@ -19,3 +23,44 @@ async def publish_nostr_event(e: NostrEvent): ) except Exception as ex: logger.warning(ex) + + +async def connect_to_nostrclient_ws( + on_open: Callable, on_message: Callable +) -> WebSocketApp: + def on_error(_, error): + logger.warning(error) + + logger.debug(f"Subscribing to websockets for nostrclient extension") + ws = WebSocketApp( + f"ws://localhost:{settings.port}/nostrclient/api/v1/filters", + on_message=on_message, + on_open=on_open, + on_error=on_error, + ) + + wst = Thread(target=ws.run_forever) + wst.daemon = True + wst.start() + + return ws + + +async def handle_event(event, pubkeys): + tags = [t[1] for t in event["tags"] if t[0] == "p"] + to_merchant = None + if tags and len(tags) > 0: + to_merchant = tags[0] + + if event["pubkey"] in pubkeys or to_merchant in pubkeys: + logger.debug(f"Event sent to {to_merchant}") + pubkey = to_merchant if to_merchant in pubkeys else event["pubkey"] + # Send event to market extension + await send_event_to_market(event=event, pubkey=pubkey) + + +async def send_event_to_market(event: dict, pubkey: str): + # Sends event to market extension, for decrypt and handling + market_url = url_for(f"/market/api/v1/nip04/{pubkey}", external=True) + async with httpx.AsyncClient() as client: + await client.post(url=market_url, json=event) diff --git a/tasks.py b/tasks.py index 3254dcc..901504c 100644 --- a/tasks.py +++ b/tasks.py @@ -1,18 +1,25 @@ import asyncio import json -import threading +from asyncio import Queue import httpx import websocket from loguru import logger +from websocket import WebSocketApp from lnbits.core.models import Payment -from lnbits.helpers import url_for from lnbits.tasks import register_invoice_listener +from .crud import get_merchant, get_merchant_by_pubkey, get_public_keys_for_merchants +from .nostr.event import NostrEvent +from .nostr.nostr_client import connect_to_nostrclient_ws + +recieve_event_queue: Queue = Queue() +send_req_queue: Queue = Queue() + async def wait_for_paid_invoices(): - invoice_queue = asyncio.Queue() + invoice_queue = Queue() register_invoice_listener(invoice_queue) while True: @@ -27,5 +34,61 @@ async def on_invoice_paid(payment: Payment) -> None: print("### on_invoice_paid") -async def subscribe_nostrclient_ws(): +async def subscribe_nostrclient(): print("### subscribe_nostrclient_ws") + + def on_open(_): + logger.info("Connected to 'nostrclient' websocket") + + def on_message(_, message): + print("### on_message", message) + recieve_event_queue.put_nowait(message) + + # wait for 'nostrclient' extension to initialize + await asyncio.sleep(5) + ws: WebSocketApp = None + while True: + try: + req = None + if not ws: + ws = await connect_to_nostrclient_ws(on_open, on_message) + # be sure the connection is open + await asyncio.sleep(3) + req = await send_req_queue.get() + print("### req", req) + ws.send(json.dumps(req)) + except Exception as ex: + logger.warning(ex) + if req: + await send_req_queue.put(req) + ws = None # todo close + await asyncio.sleep(5) + + +async def wait_for_nostr_events(): + public_keys = await get_public_keys_for_merchants() + for p in public_keys: + await send_req_queue.put( + ["REQ", f"direct-messages:{p}", {"kind": 4, "#p": [p]}] + ) + + while True: + message = await recieve_event_queue.get() + await handle_message(message) + + +async def handle_message(msg: str): + try: + type, subscription_id, event = json.loads(msg) + _, public_key = subscription_id.split(":") + if type.upper() == "EVENT": + event = NostrEvent(**event) + if event.kind == 4: + merchant = await get_merchant_by_pubkey(public_key) + if not merchant: + return + clear_text_msg = merchant.decrypt_message(event.content, event.pubkey) + print("### clear_text_msg", clear_text_msg) + + except Exception as ex: + logger.warning(ex) diff --git a/views_api.py b/views_api.py index 4680977..ecb14f9 100644 --- a/views_api.py +++ b/views_api.py @@ -12,7 +12,6 @@ from lnbits.decorators import ( require_admin_key, require_invoice_key, ) -from lnbits.extensions.nostrmarket.nostr.event import NostrEvent from lnbits.utils.exchange_rates import currencies from . import nostrmarket_ext @@ -46,6 +45,7 @@ from .models import ( Stall, Zone, ) +from .nostr.event import NostrEvent from .nostr.nostr_client import publish_nostr_event ######################################## MERCHANT ######################################## From 3988933e402f6c40dfda04e4e36be689022e344d Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Fri, 3 Mar 2023 18:24:53 +0200 Subject: [PATCH 130/891] feat: listen for direct messages --- __init__.py | 10 +++--- crud.py | 17 +++++++++++ models.py | 6 +++- nostr/nostr_client.py | 47 +++++++++++++++++++++++++++- tasks.py | 71 ++++++++++++++++++++++++++++++++++++++++--- views_api.py | 2 +- 6 files changed, 142 insertions(+), 11 deletions(-) diff --git a/__init__.py b/__init__.py index 9e188f2..9f3a39e 100644 --- a/__init__.py +++ b/__init__.py @@ -1,4 +1,5 @@ import asyncio +from asyncio import Task from typing import List from fastapi import APIRouter @@ -25,9 +26,9 @@ def nostrmarket_renderer(): return template_renderer(["lnbits/extensions/nostrmarket/templates"]) -scheduled_tasks: List[asyncio.Task] = [] +scheduled_tasks: List[Task] = [] -from .tasks import subscribe_nostrclient_ws, wait_for_paid_invoices +from .tasks import subscribe_nostrclient, wait_for_nostr_events, wait_for_paid_invoices from .views import * # noqa from .views_api import * # noqa @@ -35,5 +36,6 @@ from .views_api import * # noqa def nostrmarket_start(): loop = asyncio.get_event_loop() task1 = loop.create_task(catch_everything_and_restart(wait_for_paid_invoices)) - task2 = loop.create_task(catch_everything_and_restart(subscribe_nostrclient_ws)) - scheduled_tasks.append([task1, task2]) + task2 = loop.create_task(catch_everything_and_restart(subscribe_nostrclient)) + task3 = loop.create_task(catch_everything_and_restart(wait_for_nostr_events)) + scheduled_tasks.append([task1, task2, task3]) diff --git a/crud.py b/crud.py index 990b9ad..edd9a09 100644 --- a/crud.py +++ b/crud.py @@ -45,6 +45,23 @@ async def get_merchant(user_id: str, merchant_id: str) -> Optional[Merchant]: return Merchant.from_row(row) if row else None +async def get_merchant_by_pubkey(public_key: str) -> Optional[Merchant]: + row = await db.fetchone( + """SELECT * FROM nostrmarket.merchants WHERE public_key = ? """, + (public_key,), + ) + + return Merchant.from_row(row) if row else None + + +async def get_public_keys_for_merchants() -> List[str]: + rows = await db.fetchall( + """SELECT public_key FROM nostrmarket.merchants""", + ) + + return [row[0] for row in rows] + + async def get_merchant_for_user(user_id: str) -> Optional[Merchant]: row = await db.fetchone( """SELECT * FROM nostrmarket.merchants WHERE user_id = ? """, diff --git a/models.py b/models.py index 194cf04..58b093d 100644 --- a/models.py +++ b/models.py @@ -6,7 +6,7 @@ from typing import List, Optional from pydantic import BaseModel -from .helpers import sign_message_hash +from .helpers import decrypt_message, get_shared_secret, sign_message_hash from .nostr.event import NostrEvent ######################################## NOSTR ######################################## @@ -39,6 +39,10 @@ class Merchant(PartialMerchant): def sign_hash(self, hash: bytes) -> str: return sign_message_hash(self.private_key, hash) + def decrypt_message(self, encrypted_message: str, public_key: str) -> str: + encryption_key = get_shared_secret(self.private_key, public_key) + return decrypt_message(encrypted_message, encryption_key) + @classmethod def from_row(cls, row: Row) -> "Merchant": merchant = cls(**dict(row)) diff --git a/nostr/nostr_client.py b/nostr/nostr_client.py index 306c123..bb64c58 100644 --- a/nostr/nostr_client.py +++ b/nostr/nostr_client.py @@ -1,5 +1,9 @@ +from threading import Thread +from typing import Callable + import httpx from loguru import logger +from websocket import WebSocketApp from lnbits.app import settings from lnbits.helpers import url_for @@ -10,7 +14,7 @@ from .event import NostrEvent async def publish_nostr_event(e: NostrEvent): url = url_for("/nostrclient/api/v1/publish", external=True) data = dict(e) - # print("### published", dict(data)) + print("### published", dict(data)) async with httpx.AsyncClient() as client: try: await client.post( @@ -19,3 +23,44 @@ async def publish_nostr_event(e: NostrEvent): ) except Exception as ex: logger.warning(ex) + + +async def connect_to_nostrclient_ws( + on_open: Callable, on_message: Callable +) -> WebSocketApp: + def on_error(_, error): + logger.warning(error) + + logger.debug(f"Subscribing to websockets for nostrclient extension") + ws = WebSocketApp( + f"ws://localhost:{settings.port}/nostrclient/api/v1/filters", + on_message=on_message, + on_open=on_open, + on_error=on_error, + ) + + wst = Thread(target=ws.run_forever) + wst.daemon = True + wst.start() + + return ws + + +async def handle_event(event, pubkeys): + tags = [t[1] for t in event["tags"] if t[0] == "p"] + to_merchant = None + if tags and len(tags) > 0: + to_merchant = tags[0] + + if event["pubkey"] in pubkeys or to_merchant in pubkeys: + logger.debug(f"Event sent to {to_merchant}") + pubkey = to_merchant if to_merchant in pubkeys else event["pubkey"] + # Send event to market extension + await send_event_to_market(event=event, pubkey=pubkey) + + +async def send_event_to_market(event: dict, pubkey: str): + # Sends event to market extension, for decrypt and handling + market_url = url_for(f"/market/api/v1/nip04/{pubkey}", external=True) + async with httpx.AsyncClient() as client: + await client.post(url=market_url, json=event) diff --git a/tasks.py b/tasks.py index 3254dcc..901504c 100644 --- a/tasks.py +++ b/tasks.py @@ -1,18 +1,25 @@ import asyncio import json -import threading +from asyncio import Queue import httpx import websocket from loguru import logger +from websocket import WebSocketApp from lnbits.core.models import Payment -from lnbits.helpers import url_for from lnbits.tasks import register_invoice_listener +from .crud import get_merchant, get_merchant_by_pubkey, get_public_keys_for_merchants +from .nostr.event import NostrEvent +from .nostr.nostr_client import connect_to_nostrclient_ws + +recieve_event_queue: Queue = Queue() +send_req_queue: Queue = Queue() + async def wait_for_paid_invoices(): - invoice_queue = asyncio.Queue() + invoice_queue = Queue() register_invoice_listener(invoice_queue) while True: @@ -27,5 +34,61 @@ async def on_invoice_paid(payment: Payment) -> None: print("### on_invoice_paid") -async def subscribe_nostrclient_ws(): +async def subscribe_nostrclient(): print("### subscribe_nostrclient_ws") + + def on_open(_): + logger.info("Connected to 'nostrclient' websocket") + + def on_message(_, message): + print("### on_message", message) + recieve_event_queue.put_nowait(message) + + # wait for 'nostrclient' extension to initialize + await asyncio.sleep(5) + ws: WebSocketApp = None + while True: + try: + req = None + if not ws: + ws = await connect_to_nostrclient_ws(on_open, on_message) + # be sure the connection is open + await asyncio.sleep(3) + req = await send_req_queue.get() + print("### req", req) + ws.send(json.dumps(req)) + except Exception as ex: + logger.warning(ex) + if req: + await send_req_queue.put(req) + ws = None # todo close + await asyncio.sleep(5) + + +async def wait_for_nostr_events(): + public_keys = await get_public_keys_for_merchants() + for p in public_keys: + await send_req_queue.put( + ["REQ", f"direct-messages:{p}", {"kind": 4, "#p": [p]}] + ) + + while True: + message = await recieve_event_queue.get() + await handle_message(message) + + +async def handle_message(msg: str): + try: + type, subscription_id, event = json.loads(msg) + _, public_key = subscription_id.split(":") + if type.upper() == "EVENT": + event = NostrEvent(**event) + if event.kind == 4: + merchant = await get_merchant_by_pubkey(public_key) + if not merchant: + return + clear_text_msg = merchant.decrypt_message(event.content, event.pubkey) + print("### clear_text_msg", clear_text_msg) + + except Exception as ex: + logger.warning(ex) diff --git a/views_api.py b/views_api.py index 4680977..ecb14f9 100644 --- a/views_api.py +++ b/views_api.py @@ -12,7 +12,6 @@ from lnbits.decorators import ( require_admin_key, require_invoice_key, ) -from lnbits.extensions.nostrmarket.nostr.event import NostrEvent from lnbits.utils.exchange_rates import currencies from . import nostrmarket_ext @@ -46,6 +45,7 @@ from .models import ( Stall, Zone, ) +from .nostr.event import NostrEvent from .nostr.nostr_client import publish_nostr_event ######################################## MERCHANT ######################################## From 8a2bc0e34563e1d05f7e349e35203d259a802608 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Fri, 3 Mar 2023 16:53:35 +0000 Subject: [PATCH 131/891] routing done --- .../customer-market/customer-market.html | 2 +- .../customer-market/customer-market.js | 6 +- .../customer-stall/customer-stall.html | 93 ++------------ .../customer-stall/customer-stall.js | 44 ++++--- .../components/product-card/product-card.html | 1 + .../components/product-card/product-card.js | 2 +- .../product-detail/product-detail.html | 118 +----------------- static/js/market.js | 76 +++++------ templates/nostrmarket/market.html | 3 +- 9 files changed, 78 insertions(+), 267 deletions(-) diff --git a/static/components/customer-market/customer-market.html b/static/components/customer-market/customer-market.html index 5de0b0d..ce52d87 100644 --- a/static/components/customer-market/customer-market.html +++ b/static/components/customer-market/customer-market.html @@ -10,7 +10,7 @@ v-for="(item, idx) in products" :key="idx" > - +
diff --git a/static/components/customer-market/customer-market.js b/static/components/customer-market/customer-market.js index 992222a..c14ffa9 100644 --- a/static/components/customer-market/customer-market.js +++ b/static/components/customer-market/customer-market.js @@ -8,7 +8,11 @@ async function customerMarket(path) { data: function () { return {} }, - methods: {}, + methods: { + changePageM(page, opts) { + this.$emit('change-page', page, opts) + } + }, created() {} }) } diff --git a/static/components/customer-stall/customer-stall.html b/static/components/customer-stall/customer-stall.html index 306a5f7..9bc302c 100644 --- a/static/components/customer-stall/customer-stall.html +++ b/static/components/customer-stall/customer-stall.html @@ -10,94 +10,25 @@ - + - +
+ +
+ +
+
- - - - - Add to cart -
-
- {{ item.name }} -
-
- - -
- - -
-
- {{ item.stallName }} -
- - {{ item.price }} satsBTC {{ (item.price / 1e8).toFixed(8) }} - - - {{ item.formatedPrice }} - ({{ item.priceInSats }} sats) - - {{ item.amount }} left -
-
- {{cat}} -
-
-

{{ item.description }}

-
-
- - - - - Stall: {{ item.stallName }} -
- See product -
-
-
+
diff --git a/static/components/customer-stall/customer-stall.js b/static/components/customer-stall/customer-stall.js index 7606d68..1d9b395 100644 --- a/static/components/customer-stall/customer-stall.js +++ b/static/components/customer-stall/customer-stall.js @@ -1,34 +1,32 @@ async function customerStall(path) { const template = await loadTemplateAsync(path) - const mock = { - stall: '4M8j9KKGzUckHgb4C3pKCv', - name: 'product 1', - description: - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Leo integer malesuada nunc vel risus commodo. Sapien faucibus et molestie ac feugiat sed lectus vestibulum mattis. Cras ornare arcu dui vivamus. Risus pretium quam vulputate dignissim suspendisse in est ante in. Faucibus in ornare quam viverra orci sagittis eu volutpat odio.', - amount: 100, - price: '10', - images: ['https://i.imgur.com/cEfpEjq.jpeg'], - id: ['RyMbE9Hdwk9X333JKtkkNS'], - categories: ['crafts', 'robots'], - currency: 'EUR', - stallName: 'stall 1', - formatedPrice: '€10.00', - priceInSats: 0 - } + Vue.component('customer-stall', { name: 'customer-stall', template, - props: ['stall', 'products', 'exchange-rates', 'product-detail'], + props: [ + 'stall', + 'products', + 'exchange-rates', + 'product-detail', + 'change-page' + ], data: function () { - return { - mock: mock + return {} + }, + computed: { + product() { + if (this.productDetail) { + return this.products.find(p => p.id == this.productDetail) + } } }, - methods: {}, - created() { - console.log(this.stall) - console.log(this.products) - } + methods: { + changePageS(page, opts) { + this.$emit('change-page', page, opts) + } + }, + created() {} }) } diff --git a/static/components/product-card/product-card.html b/static/components/product-card/product-card.html index 5ff2782..93d4d96 100644 --- a/static/components/product-card/product-card.html +++ b/static/components/product-card/product-card.html @@ -52,6 +52,7 @@ Stall: {{ product.stallName }} + {{ $parent.activeStall }}
{{ product.amount > 0 ? 'In stock.' : 'Out of stock.' }} -
-
-
Customer rating
-
4.2
-
- -
-
(357 reviews)
-
- 93% would recommend to a friend -
- - - - 5 - - - 273 - - - 4 - - -   69 - - - 3 - - -      6 - - - 2 - - -      3 - - - 1 - - -      6 - - -
-
- -
diff --git a/static/js/market.js b/static/js/market.js index 3c823fc..6a0d723 100644 --- a/static/js/market.js +++ b/static/js/market.js @@ -30,7 +30,8 @@ const market = async () => { productCard('static/components/product-card/product-card.html'), customerMarket('static/components/customer-market/customer-market.html'), customerStall('static/components/customer-stall/customer-stall.html'), - productDetail('static/components/product-detail/product-detail.html') + productDetail('static/components/product-detail/product-detail.html'), + shoppingCart('static/components/shopping-cart/shopping-cart.html') ]) new Vue({ @@ -58,7 +59,7 @@ const market = async () => { filterProducts() { let products = this.products if (this.activeStall) { - products = products.filter(p => p.stall == this.activeStall) + products = products.filter(p => p.stall_id == this.activeStall) } if (!this.searchText || this.searchText.length < 2) return products return products.filter(p => { @@ -76,6 +77,9 @@ const market = async () => { return ( this.products.find(p => p.id == this.activeProduct)?.name || 'Product' ) + }, + isLoading() { + return this.$q.loading.isActive } }, async created() { @@ -88,51 +92,17 @@ const market = async () => { } if (relays && relays.length) { this.relays = new Set([...defaultRelays, ...relays]) + } else { + this.relays = new Set(defaultRelays) } } catch (e) { console.error(e) } - // Hardcode pubkeys for testing - /* - this.pubkeys.add( - 'c1415f950a1e3431de2bc5ee35144639e2f514cf158279abff9ed77d50118796' - ) - this.pubkeys.add( - '8f69ac99b96f7c4ad58b98cc38fe5d35ce02daefae7d1609c797ce3b4f92f5fd' - ) - */ - // stall ids S4hQgtTwiF5kGJZPbqMH9M jkCbdtkXeMjGBY3LBf8yn4 - /*let naddr = nostr.nip19.naddrEncode({ - identifier: '1234', - pubkey: - 'c1415f950a1e3431de2bc5ee35144639e2f514cf158279abff9ed77d50118796', - kind: 30018, - relays: defaultRelays - }) - console.log(naddr) - console.log(nostr.nip19.decode(naddr)) - */ + let params = new URLSearchParams(window.location.search) let merchant_pubkey = params.get('merchant_pubkey') let stall_id = params.get('stall_id') let product_id = params.get('product_id') - console.log(merchant_pubkey, stall_id, product_id) - if (merchant_pubkey) { - await addPubkey(merchant_pubkey) - /*LNbits.utils - .confirmDialog( - `We found a merchant pubkey in your request. Do you want to add it to the merchants list?` - ) - .onCancel(() => {}) - .onDismiss(() => {}) - .onOk(() => { - this.pubkeys.add(merchant_pubkey) - })*/ - } - this.$q.loading.show() - this.relays = new Set(defaultRelays) - // Get notes from Nostr - await this.initNostr() // What component to render on start if (stall_id) { @@ -142,11 +112,33 @@ const market = async () => { this.activePage = 'stall' this.activeStall = stall_id } + if (merchant_pubkey && !this.pubkeys.has(merchant_pubkey)) { + await LNbits.utils + .confirmDialog( + `We found a merchant pubkey in your request. Do you want to add it to the merchants list?` + ) + .onOk(async () => { + await this.addPubkey(merchant_pubkey) + }) + } + // Get notes from Nostr + await this.initNostr() this.$q.loading.hide() }, methods: { + naddr() { + let naddr = nostr.nip19.naddrEncode({ + identifier: '1234', + pubkey: + 'c1415f950a1e3431de2bc5ee35144639e2f514cf158279abff9ed77d50118796', + kind: 30018, + relays: defaultRelays + }) + console.log(naddr) + }, async initNostr() { + this.$q.loading.show() const pool = new nostr.SimplePool() let relays = Array.from(this.relays) let products = new Map() @@ -168,10 +160,10 @@ const market = async () => { return } else if (e.kind == 30018) { //it's a product `d` is the prod. id - products.set(e.d, {...e.content, id: e.d, categories: e.t}) + products.set(e.d, {...e.content, id: e.d[0], categories: e.t}) } else if (e.kind == 30017) { // it's a stall `d` is the stall id - stalls.set(e.d, {...e.content, id: e.d, pubkey: e.pubkey}) + stalls.set(e.d, {...e.content, id: e.d[0], pubkey: e.pubkey}) return } }) @@ -182,13 +174,13 @@ const market = async () => { this.products = Array.from(products.values()).map(obj => { let stall = this.stalls.find(s => s.id == obj.stall_id) obj.stallName = stall.name + obj.images = [obj.image] if (obj.currency != 'sat') { obj.formatedPrice = this.getAmountFormated(obj.price, obj.currency) obj.priceInSats = this.getValueInSats(obj.price, obj.currency) } return obj }) - pool.close(relays) }, async getRates() { diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html index 0d3c36e..c885e10 100644 --- a/templates/nostrmarket/market.html +++ b/templates/nostrmarket/market.html @@ -140,7 +140,7 @@ + {% endblock %} From ac6df4041891671be0d54135587f3cfff8474f19 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Fri, 3 Mar 2023 16:53:35 +0000 Subject: [PATCH 132/891] routing done --- .../customer-market/customer-market.html | 2 +- .../customer-market/customer-market.js | 6 +- .../customer-stall/customer-stall.html | 93 ++------------ .../customer-stall/customer-stall.js | 44 ++++--- .../components/product-card/product-card.html | 1 + .../components/product-card/product-card.js | 2 +- .../product-detail/product-detail.html | 118 +----------------- static/js/market.js | 76 +++++------ templates/nostrmarket/market.html | 3 +- 9 files changed, 78 insertions(+), 267 deletions(-) diff --git a/static/components/customer-market/customer-market.html b/static/components/customer-market/customer-market.html index 5de0b0d..ce52d87 100644 --- a/static/components/customer-market/customer-market.html +++ b/static/components/customer-market/customer-market.html @@ -10,7 +10,7 @@ v-for="(item, idx) in products" :key="idx" > - + diff --git a/static/components/customer-market/customer-market.js b/static/components/customer-market/customer-market.js index 992222a..c14ffa9 100644 --- a/static/components/customer-market/customer-market.js +++ b/static/components/customer-market/customer-market.js @@ -8,7 +8,11 @@ async function customerMarket(path) { data: function () { return {} }, - methods: {}, + methods: { + changePageM(page, opts) { + this.$emit('change-page', page, opts) + } + }, created() {} }) } diff --git a/static/components/customer-stall/customer-stall.html b/static/components/customer-stall/customer-stall.html index 306a5f7..9bc302c 100644 --- a/static/components/customer-stall/customer-stall.html +++ b/static/components/customer-stall/customer-stall.html @@ -10,94 +10,25 @@ - + - +
+ +
+ +
+
- - - - - Add to cart -
-
- {{ item.name }} -
-
- - -
- - -
-
- {{ item.stallName }} -
- - {{ item.price }} satsBTC {{ (item.price / 1e8).toFixed(8) }} - - - {{ item.formatedPrice }} - ({{ item.priceInSats }} sats) - - {{ item.amount }} left -
-
- {{cat}} -
-
-

{{ item.description }}

-
-
- - - - - Stall: {{ item.stallName }} -
- See product -
-
-
+
diff --git a/static/components/customer-stall/customer-stall.js b/static/components/customer-stall/customer-stall.js index 7606d68..1d9b395 100644 --- a/static/components/customer-stall/customer-stall.js +++ b/static/components/customer-stall/customer-stall.js @@ -1,34 +1,32 @@ async function customerStall(path) { const template = await loadTemplateAsync(path) - const mock = { - stall: '4M8j9KKGzUckHgb4C3pKCv', - name: 'product 1', - description: - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Leo integer malesuada nunc vel risus commodo. Sapien faucibus et molestie ac feugiat sed lectus vestibulum mattis. Cras ornare arcu dui vivamus. Risus pretium quam vulputate dignissim suspendisse in est ante in. Faucibus in ornare quam viverra orci sagittis eu volutpat odio.', - amount: 100, - price: '10', - images: ['https://i.imgur.com/cEfpEjq.jpeg'], - id: ['RyMbE9Hdwk9X333JKtkkNS'], - categories: ['crafts', 'robots'], - currency: 'EUR', - stallName: 'stall 1', - formatedPrice: '€10.00', - priceInSats: 0 - } + Vue.component('customer-stall', { name: 'customer-stall', template, - props: ['stall', 'products', 'exchange-rates', 'product-detail'], + props: [ + 'stall', + 'products', + 'exchange-rates', + 'product-detail', + 'change-page' + ], data: function () { - return { - mock: mock + return {} + }, + computed: { + product() { + if (this.productDetail) { + return this.products.find(p => p.id == this.productDetail) + } } }, - methods: {}, - created() { - console.log(this.stall) - console.log(this.products) - } + methods: { + changePageS(page, opts) { + this.$emit('change-page', page, opts) + } + }, + created() {} }) } diff --git a/static/components/product-card/product-card.html b/static/components/product-card/product-card.html index 5ff2782..93d4d96 100644 --- a/static/components/product-card/product-card.html +++ b/static/components/product-card/product-card.html @@ -52,6 +52,7 @@ Stall: {{ product.stallName }} + {{ $parent.activeStall }}
{{ product.amount > 0 ? 'In stock.' : 'Out of stock.' }} -
-
-
Customer rating
-
4.2
-
- -
-
(357 reviews)
-
- 93% would recommend to a friend -
- - - - 5 - - - 273 - - - 4 - - -   69 - - - 3 - - -      6 - - - 2 - - -      3 - - - 1 - - -      6 - - -
-
- -
diff --git a/static/js/market.js b/static/js/market.js index 3c823fc..6a0d723 100644 --- a/static/js/market.js +++ b/static/js/market.js @@ -30,7 +30,8 @@ const market = async () => { productCard('static/components/product-card/product-card.html'), customerMarket('static/components/customer-market/customer-market.html'), customerStall('static/components/customer-stall/customer-stall.html'), - productDetail('static/components/product-detail/product-detail.html') + productDetail('static/components/product-detail/product-detail.html'), + shoppingCart('static/components/shopping-cart/shopping-cart.html') ]) new Vue({ @@ -58,7 +59,7 @@ const market = async () => { filterProducts() { let products = this.products if (this.activeStall) { - products = products.filter(p => p.stall == this.activeStall) + products = products.filter(p => p.stall_id == this.activeStall) } if (!this.searchText || this.searchText.length < 2) return products return products.filter(p => { @@ -76,6 +77,9 @@ const market = async () => { return ( this.products.find(p => p.id == this.activeProduct)?.name || 'Product' ) + }, + isLoading() { + return this.$q.loading.isActive } }, async created() { @@ -88,51 +92,17 @@ const market = async () => { } if (relays && relays.length) { this.relays = new Set([...defaultRelays, ...relays]) + } else { + this.relays = new Set(defaultRelays) } } catch (e) { console.error(e) } - // Hardcode pubkeys for testing - /* - this.pubkeys.add( - 'c1415f950a1e3431de2bc5ee35144639e2f514cf158279abff9ed77d50118796' - ) - this.pubkeys.add( - '8f69ac99b96f7c4ad58b98cc38fe5d35ce02daefae7d1609c797ce3b4f92f5fd' - ) - */ - // stall ids S4hQgtTwiF5kGJZPbqMH9M jkCbdtkXeMjGBY3LBf8yn4 - /*let naddr = nostr.nip19.naddrEncode({ - identifier: '1234', - pubkey: - 'c1415f950a1e3431de2bc5ee35144639e2f514cf158279abff9ed77d50118796', - kind: 30018, - relays: defaultRelays - }) - console.log(naddr) - console.log(nostr.nip19.decode(naddr)) - */ + let params = new URLSearchParams(window.location.search) let merchant_pubkey = params.get('merchant_pubkey') let stall_id = params.get('stall_id') let product_id = params.get('product_id') - console.log(merchant_pubkey, stall_id, product_id) - if (merchant_pubkey) { - await addPubkey(merchant_pubkey) - /*LNbits.utils - .confirmDialog( - `We found a merchant pubkey in your request. Do you want to add it to the merchants list?` - ) - .onCancel(() => {}) - .onDismiss(() => {}) - .onOk(() => { - this.pubkeys.add(merchant_pubkey) - })*/ - } - this.$q.loading.show() - this.relays = new Set(defaultRelays) - // Get notes from Nostr - await this.initNostr() // What component to render on start if (stall_id) { @@ -142,11 +112,33 @@ const market = async () => { this.activePage = 'stall' this.activeStall = stall_id } + if (merchant_pubkey && !this.pubkeys.has(merchant_pubkey)) { + await LNbits.utils + .confirmDialog( + `We found a merchant pubkey in your request. Do you want to add it to the merchants list?` + ) + .onOk(async () => { + await this.addPubkey(merchant_pubkey) + }) + } + // Get notes from Nostr + await this.initNostr() this.$q.loading.hide() }, methods: { + naddr() { + let naddr = nostr.nip19.naddrEncode({ + identifier: '1234', + pubkey: + 'c1415f950a1e3431de2bc5ee35144639e2f514cf158279abff9ed77d50118796', + kind: 30018, + relays: defaultRelays + }) + console.log(naddr) + }, async initNostr() { + this.$q.loading.show() const pool = new nostr.SimplePool() let relays = Array.from(this.relays) let products = new Map() @@ -168,10 +160,10 @@ const market = async () => { return } else if (e.kind == 30018) { //it's a product `d` is the prod. id - products.set(e.d, {...e.content, id: e.d, categories: e.t}) + products.set(e.d, {...e.content, id: e.d[0], categories: e.t}) } else if (e.kind == 30017) { // it's a stall `d` is the stall id - stalls.set(e.d, {...e.content, id: e.d, pubkey: e.pubkey}) + stalls.set(e.d, {...e.content, id: e.d[0], pubkey: e.pubkey}) return } }) @@ -182,13 +174,13 @@ const market = async () => { this.products = Array.from(products.values()).map(obj => { let stall = this.stalls.find(s => s.id == obj.stall_id) obj.stallName = stall.name + obj.images = [obj.image] if (obj.currency != 'sat') { obj.formatedPrice = this.getAmountFormated(obj.price, obj.currency) obj.priceInSats = this.getValueInSats(obj.price, obj.currency) } return obj }) - pool.close(relays) }, async getRates() { diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html index 0d3c36e..c885e10 100644 --- a/templates/nostrmarket/market.html +++ b/templates/nostrmarket/market.html @@ -140,7 +140,7 @@ + {% endblock %} From 1b5079a88d31f53d4b2cce8151bfc6da7ab267f6 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Fri, 3 Mar 2023 16:53:46 +0000 Subject: [PATCH 133/891] init shopping cart --- .../components/shopping-cart/shopping-cart.html | 1 + static/components/shopping-cart/shopping-cart.js | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 static/components/shopping-cart/shopping-cart.html create mode 100644 static/components/shopping-cart/shopping-cart.js diff --git a/static/components/shopping-cart/shopping-cart.html b/static/components/shopping-cart/shopping-cart.html new file mode 100644 index 0000000..8650cc6 --- /dev/null +++ b/static/components/shopping-cart/shopping-cart.html @@ -0,0 +1 @@ + diff --git a/static/components/shopping-cart/shopping-cart.js b/static/components/shopping-cart/shopping-cart.js new file mode 100644 index 0000000..e0ad053 --- /dev/null +++ b/static/components/shopping-cart/shopping-cart.js @@ -0,0 +1,16 @@ +async function shoppingCart(path) { + const template = await loadTemplateAsync(path) + + Vue.component('shopping-cart', { + name: 'shopping-cart', + template, + + props: [], + data: function () { + return {} + }, + computed: {}, + methods: {}, + created() {} + }) +} From cbe4d32d8a05dd34920dac28a45c03fa5160375a Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Fri, 3 Mar 2023 16:53:46 +0000 Subject: [PATCH 134/891] init shopping cart --- .../components/shopping-cart/shopping-cart.html | 1 + static/components/shopping-cart/shopping-cart.js | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 static/components/shopping-cart/shopping-cart.html create mode 100644 static/components/shopping-cart/shopping-cart.js diff --git a/static/components/shopping-cart/shopping-cart.html b/static/components/shopping-cart/shopping-cart.html new file mode 100644 index 0000000..8650cc6 --- /dev/null +++ b/static/components/shopping-cart/shopping-cart.html @@ -0,0 +1 @@ + diff --git a/static/components/shopping-cart/shopping-cart.js b/static/components/shopping-cart/shopping-cart.js new file mode 100644 index 0000000..e0ad053 --- /dev/null +++ b/static/components/shopping-cart/shopping-cart.js @@ -0,0 +1,16 @@ +async function shoppingCart(path) { + const template = await loadTemplateAsync(path) + + Vue.component('shopping-cart', { + name: 'shopping-cart', + template, + + props: [], + data: function () { + return {} + }, + computed: {}, + methods: {}, + created() {} + }) +} From 7ae14aa32f143c85bac18b897e030c584d081e8d Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Sat, 4 Mar 2023 19:49:58 +0000 Subject: [PATCH 135/891] general fixes, prop passing and shopping cart (doesn't send dm) --- .../customer-stall/customer-stall.html | 106 ++++++++++- .../customer-stall/customer-stall.js | 167 +++++++++++++++++- .../components/product-card/product-card.html | 16 ++ .../components/product-card/product-card.js | 2 +- .../product-detail/product-detail.html | 12 +- .../product-detail/product-detail.js | 15 +- .../shopping-cart/shopping-cart.html | 52 +++++- .../components/shopping-cart/shopping-cart.js | 2 +- static/js/market.js | 17 +- templates/nostrmarket/market.html | 1 + templates/nostrmarket/stall.html | 61 ------- 11 files changed, 355 insertions(+), 96 deletions(-) delete mode 100644 templates/nostrmarket/stall.html diff --git a/static/components/customer-stall/customer-stall.html b/static/components/customer-stall/customer-stall.html index 9bc302c..aedec55 100644 --- a/static/components/customer-stall/customer-stall.html +++ b/static/components/customer-stall/customer-stall.html @@ -10,13 +10,20 @@ - +
@@ -28,7 +35,102 @@ v-for="(item, idx) in products" :key="idx" > - +
+ + + + + + + + + +
+
+ Generate key pair +
+
+ Get from Extension +
+
+ + +

Select the shipping zone:

+
+ +
+
+ Total: {{ stall.currency != 'sat' ? getAmountFormated(finalCost) : + finalCost + 'sats' }} + ({{ getValueInSats(finalCost) }} sats) +
+
+ Checkout + Cancel +
+
+
+
+ diff --git a/static/components/customer-stall/customer-stall.js b/static/components/customer-stall/customer-stall.js index 1d9b395..858ed9d 100644 --- a/static/components/customer-stall/customer-stall.js +++ b/static/components/customer-stall/customer-stall.js @@ -10,23 +10,184 @@ async function customerStall(path) { 'products', 'exchange-rates', 'product-detail', - 'change-page' + 'change-page', + 'relays' ], data: function () { - return {} + return { + cart: { + total: 0, + size: 0, + products: new Map() + }, + cartMenu: [], + hasNip07: false, + checkoutDialog: { + show: false, + data: { + pubkey: null + } + }, + qrCodeDialog: { + data: { + payment_request: null + }, + show: false + } + } }, computed: { product() { if (this.productDetail) { return this.products.find(p => p.id == this.productDetail) } + }, + finalCost() { + if (!this.checkoutDialog.data.shippingzone) return this.cart.total + + let zoneCost = this.stall.shipping.find( + z => z.id == this.checkoutDialog.data.shippingzone + ) + return +this.cart.total + zoneCost.cost } }, methods: { changePageS(page, opts) { this.$emit('change-page', page, opts) + }, + getValueInSats(amount, unit = 'USD') { + if (!this.exchangeRates) return 0 + return Math.ceil( + (amount / this.exchangeRates[`BTC${unit}`][unit]) * 1e8 + ) + }, + getAmountFormated(amount, unit = 'USD') { + return LNbits.utils.formatCurrency(amount, unit) + }, + addToCart(item) { + console.log('add to cart', item) + let prod = this.cart.products + if (prod.has(item.id)) { + let qty = prod.get(item.id).quantity + prod.set(item.id, { + ...prod.get(item.id), + quantity: qty + 1 + }) + } else { + prod.set(item.id, { + name: item.name, + quantity: 1, + price: item.price, + image: item?.images[0] || null + }) + } + this.$q.notify({ + type: 'positive', + message: `${item.name} added to cart`, + icon: 'thumb_up' + }) + this.cart.products = prod + this.updateCart(+item.price) + }, + removeFromCart(item) { + this.cart.products.delete(item.id) + this.updateCart(+item.price, true) + }, + updateCart(price, del = false) { + console.log(this.cart, this.cartMenu) + if (del) { + this.cart.total -= price + this.cart.size-- + } else { + this.cart.total += price + this.cart.size++ + } + this.cartMenu = Array.from(this.cart.products, item => { + return {id: item[0], ...item[1]} + }) + console.log(this.cart, this.cartMenu) + }, + resetCart() { + this.cart = { + total: 0, + size: 0, + products: new Map() + } + }, + async getPubkey() { + try { + this.checkoutDialog.data.pubkey = await window.nostr.getPublicKey() + this.checkoutDialog.data.privkey = null + } catch (err) { + console.error( + `Failed to get a public key from a Nostr extension: ${err}` + ) + } + }, + generateKeyPair() { + let sk = NostrTools.generatePrivateKey() + let pk = NostrTools.getPublicKey(sk) + this.checkoutDialog.data.pubkey = pk + this.checkoutDialog.data.privkey = sk + }, + placeOrder() { + LNbits.utils + .confirmDialog( + `Send the order to the merchant? You should receive a message with the payment details.` + ) + .onOk(async () => { + let orderData = this.checkoutDialog.data + let content = { + name: orderData?.username, + description: null, + address: orderData.address, + message: null, + contact: { + nostr: orderData.pubkey, + phone: null, + email: orderData?.email + }, + items: Array.from(this.cart.products, p => { + return {product_id: p[0], quantity: p[1].quantity} + }) + } + let event = { + kind: 4, + created_at: Math.floor(Date.now() / 1000), + tags: [], + content: await window.nostr.nip04.encrypt( + orderData.pubkey, + content + ), + pubkey: orderData.pubkey + } + event.id = NostrTools.getEventHash(event) + if (orderData.privkey) { + event.sig = NostrTools.signEvent(event, orderData.privkey) + } else if (this.hasNip07) { + await window.nostr.signEvent(event) + } + await this.sendOrder(event) + }) + }, + async sendOrder(order) { + const pool = new NostrTools.SimplePool() + let relays = Array.from(this.relays) + let pubs = await pool.publish(relays, order) + pubs.on('ok', relay => { + console.log(`${relay.url} has accepted our event`) + }) + pubs.on('failed', reason => { + console.log(`failed to publish to ${reason}`) + }) } }, - created() {} + created() { + setTimeout(() => { + if (window.nostr) { + this.hasNip07 = true + } + }, 1000) + } }) } diff --git a/static/components/product-card/product-card.html b/static/components/product-card/product-card.html index 93d4d96..5a92ba3 100644 --- a/static/components/product-card/product-card.html +++ b/static/components/product-card/product-card.html @@ -9,6 +9,22 @@ > + Add to cart
{{ product.name }}
diff --git a/static/components/product-card/product-card.js b/static/components/product-card/product-card.js index 9e8490b..5e049df 100644 --- a/static/components/product-card/product-card.js +++ b/static/components/product-card/product-card.js @@ -4,7 +4,7 @@ async function productCard(path) { name: 'product-card', template, - props: ['product', 'change-page'], + props: ['product', 'change-page', 'add-to-cart', 'is-stall'], data: function () { return {} }, diff --git a/static/components/product-detail/product-detail.html b/static/components/product-detail/product-detail.html index 92fa1c0..be31eda 100644 --- a/static/components/product-detail/product-detail.html +++ b/static/components/product-detail/product-detail.html @@ -17,6 +17,11 @@ style="/*background-size: contain; background-repeat: no-repeat*/" > +
@@ -47,7 +52,7 @@ {{ product.amount > 0 ? 'In stock.' : 'Out of stock.' }}{{ product.quantity > 0 ? 'In stock.' : 'Out of stock.' }}
@@ -56,12 +61,13 @@ color="primary" icon="shopping_cart" label="Add to cart" + @click="$emit('add-to-cart', product)" />
diff --git a/static/components/product-detail/product-detail.js b/static/components/product-detail/product-detail.js index 7b60f6b..d55b653 100644 --- a/static/components/product-detail/product-detail.js +++ b/static/components/product-detail/product-detail.js @@ -4,23 +4,14 @@ async function productDetail(path) { name: 'product-detail', template, - props: ['product'], + props: ['product', 'add-to-cart'], data: function () { return { slide: 1 } }, - computed: { - win_width() { - return this.$q.screen.width - 59 - }, - win_height() { - return this.$q.screen.height - 0 - } - }, + computed: {}, methods: {}, - created() { - console.log('ping') - } + created() {} }) } diff --git a/static/components/shopping-cart/shopping-cart.html b/static/components/shopping-cart/shopping-cart.html index 8650cc6..2864cf3 100644 --- a/static/components/shopping-cart/shopping-cart.html +++ b/static/components/shopping-cart/shopping-cart.html @@ -1 +1,51 @@ - + + + {{ cart.size }} + + + + + + {{p.quantity}} x + + + + + + + + + {{ p.name }} + + + + + {{p.currency != 'sat' ? p.formatedPrice : p.price + 'sats'}} + + + + + + +
+ +
diff --git a/static/components/shopping-cart/shopping-cart.js b/static/components/shopping-cart/shopping-cart.js index e0ad053..8f6902d 100644 --- a/static/components/shopping-cart/shopping-cart.js +++ b/static/components/shopping-cart/shopping-cart.js @@ -5,7 +5,7 @@ async function shoppingCart(path) { name: 'shopping-cart', template, - props: [], + props: ['cart', 'cart-menu', 'remove-from-cart', 'reset-cart'], data: function () { return {} }, diff --git a/static/js/market.js b/static/js/market.js index 6a0d723..95bf5a6 100644 --- a/static/js/market.js +++ b/static/js/market.js @@ -1,20 +1,13 @@ const market = async () => { Vue.component(VueQrcode.name, VueQrcode) - const nostr = window.NostrTools + const NostrTools = window.NostrTools const defaultRelays = [ 'wss://relay.damus.io', 'wss://relay.snort.social', - 'wss://nos.lol', 'wss://nostr.wine', - 'wss://relay.nostr.bg', 'wss://nostr-pub.wellorder.net', - 'wss://nostr-pub.semisol.dev', - 'wss://eden.nostr.land', - 'wss://nostr.mom', - 'wss://nostr.fmt.wiz.biz', - 'wss://nostr.zebedee.cloud', - 'wss://nostr.rocks' + 'wss://nostr.zebedee.cloud' ] const eventToObj = event => { event.content = JSON.parse(event.content) @@ -128,7 +121,7 @@ const market = async () => { }, methods: { naddr() { - let naddr = nostr.nip19.naddrEncode({ + let naddr = NostrTools.nip19.naddrEncode({ identifier: '1234', pubkey: 'c1415f950a1e3431de2bc5ee35144639e2f514cf158279abff9ed77d50118796', @@ -139,7 +132,7 @@ const market = async () => { }, async initNostr() { this.$q.loading.show() - const pool = new nostr.SimplePool() + const pool = new NostrTools.SimplePool() let relays = Array.from(this.relays) let products = new Map() let stalls = new Map() @@ -243,7 +236,7 @@ const market = async () => { let regExp = /^#([0-9a-f]{3}){1,2}$/i if (pubkey.startsWith('n')) { try { - let {type, data} = nostr.nip19.decode(pubkey) + let {type, data} = NostrTools.nip19.decode(pubkey) if (type === 'npub') pubkey = data else if (type === 'nprofile') { pubkey = data.pubkey diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html index c885e10..5f01c41 100644 --- a/templates/nostrmarket/market.html +++ b/templates/nostrmarket/market.html @@ -145,6 +145,7 @@ :products="filterProducts" :exchange-rates="exchangeRates" :product-detail="activeProduct" + :relays="relays" @change-page="navigateTo" >
-{% endblock %} {% block scripts %} - - -{% endblock %} From 8d5d546884fa47496c5ae08fdb128556f09bfafd Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Sat, 4 Mar 2023 19:49:58 +0000 Subject: [PATCH 136/891] general fixes, prop passing and shopping cart (doesn't send dm) --- .../customer-stall/customer-stall.html | 106 ++++++++++- .../customer-stall/customer-stall.js | 167 +++++++++++++++++- .../components/product-card/product-card.html | 16 ++ .../components/product-card/product-card.js | 2 +- .../product-detail/product-detail.html | 12 +- .../product-detail/product-detail.js | 15 +- .../shopping-cart/shopping-cart.html | 52 +++++- .../components/shopping-cart/shopping-cart.js | 2 +- static/js/market.js | 17 +- templates/nostrmarket/market.html | 1 + templates/nostrmarket/stall.html | 61 ------- 11 files changed, 355 insertions(+), 96 deletions(-) delete mode 100644 templates/nostrmarket/stall.html diff --git a/static/components/customer-stall/customer-stall.html b/static/components/customer-stall/customer-stall.html index 9bc302c..aedec55 100644 --- a/static/components/customer-stall/customer-stall.html +++ b/static/components/customer-stall/customer-stall.html @@ -10,13 +10,20 @@ - +
@@ -28,7 +35,102 @@ v-for="(item, idx) in products" :key="idx" > - +
+ + + + + + + + + +
+
+ Generate key pair +
+
+ Get from Extension +
+
+ + +

Select the shipping zone:

+
+ +
+
+ Total: {{ stall.currency != 'sat' ? getAmountFormated(finalCost) : + finalCost + 'sats' }} + ({{ getValueInSats(finalCost) }} sats) +
+
+ Checkout + Cancel +
+
+
+
+ diff --git a/static/components/customer-stall/customer-stall.js b/static/components/customer-stall/customer-stall.js index 1d9b395..858ed9d 100644 --- a/static/components/customer-stall/customer-stall.js +++ b/static/components/customer-stall/customer-stall.js @@ -10,23 +10,184 @@ async function customerStall(path) { 'products', 'exchange-rates', 'product-detail', - 'change-page' + 'change-page', + 'relays' ], data: function () { - return {} + return { + cart: { + total: 0, + size: 0, + products: new Map() + }, + cartMenu: [], + hasNip07: false, + checkoutDialog: { + show: false, + data: { + pubkey: null + } + }, + qrCodeDialog: { + data: { + payment_request: null + }, + show: false + } + } }, computed: { product() { if (this.productDetail) { return this.products.find(p => p.id == this.productDetail) } + }, + finalCost() { + if (!this.checkoutDialog.data.shippingzone) return this.cart.total + + let zoneCost = this.stall.shipping.find( + z => z.id == this.checkoutDialog.data.shippingzone + ) + return +this.cart.total + zoneCost.cost } }, methods: { changePageS(page, opts) { this.$emit('change-page', page, opts) + }, + getValueInSats(amount, unit = 'USD') { + if (!this.exchangeRates) return 0 + return Math.ceil( + (amount / this.exchangeRates[`BTC${unit}`][unit]) * 1e8 + ) + }, + getAmountFormated(amount, unit = 'USD') { + return LNbits.utils.formatCurrency(amount, unit) + }, + addToCart(item) { + console.log('add to cart', item) + let prod = this.cart.products + if (prod.has(item.id)) { + let qty = prod.get(item.id).quantity + prod.set(item.id, { + ...prod.get(item.id), + quantity: qty + 1 + }) + } else { + prod.set(item.id, { + name: item.name, + quantity: 1, + price: item.price, + image: item?.images[0] || null + }) + } + this.$q.notify({ + type: 'positive', + message: `${item.name} added to cart`, + icon: 'thumb_up' + }) + this.cart.products = prod + this.updateCart(+item.price) + }, + removeFromCart(item) { + this.cart.products.delete(item.id) + this.updateCart(+item.price, true) + }, + updateCart(price, del = false) { + console.log(this.cart, this.cartMenu) + if (del) { + this.cart.total -= price + this.cart.size-- + } else { + this.cart.total += price + this.cart.size++ + } + this.cartMenu = Array.from(this.cart.products, item => { + return {id: item[0], ...item[1]} + }) + console.log(this.cart, this.cartMenu) + }, + resetCart() { + this.cart = { + total: 0, + size: 0, + products: new Map() + } + }, + async getPubkey() { + try { + this.checkoutDialog.data.pubkey = await window.nostr.getPublicKey() + this.checkoutDialog.data.privkey = null + } catch (err) { + console.error( + `Failed to get a public key from a Nostr extension: ${err}` + ) + } + }, + generateKeyPair() { + let sk = NostrTools.generatePrivateKey() + let pk = NostrTools.getPublicKey(sk) + this.checkoutDialog.data.pubkey = pk + this.checkoutDialog.data.privkey = sk + }, + placeOrder() { + LNbits.utils + .confirmDialog( + `Send the order to the merchant? You should receive a message with the payment details.` + ) + .onOk(async () => { + let orderData = this.checkoutDialog.data + let content = { + name: orderData?.username, + description: null, + address: orderData.address, + message: null, + contact: { + nostr: orderData.pubkey, + phone: null, + email: orderData?.email + }, + items: Array.from(this.cart.products, p => { + return {product_id: p[0], quantity: p[1].quantity} + }) + } + let event = { + kind: 4, + created_at: Math.floor(Date.now() / 1000), + tags: [], + content: await window.nostr.nip04.encrypt( + orderData.pubkey, + content + ), + pubkey: orderData.pubkey + } + event.id = NostrTools.getEventHash(event) + if (orderData.privkey) { + event.sig = NostrTools.signEvent(event, orderData.privkey) + } else if (this.hasNip07) { + await window.nostr.signEvent(event) + } + await this.sendOrder(event) + }) + }, + async sendOrder(order) { + const pool = new NostrTools.SimplePool() + let relays = Array.from(this.relays) + let pubs = await pool.publish(relays, order) + pubs.on('ok', relay => { + console.log(`${relay.url} has accepted our event`) + }) + pubs.on('failed', reason => { + console.log(`failed to publish to ${reason}`) + }) } }, - created() {} + created() { + setTimeout(() => { + if (window.nostr) { + this.hasNip07 = true + } + }, 1000) + } }) } diff --git a/static/components/product-card/product-card.html b/static/components/product-card/product-card.html index 93d4d96..5a92ba3 100644 --- a/static/components/product-card/product-card.html +++ b/static/components/product-card/product-card.html @@ -9,6 +9,22 @@ > + Add to cart
{{ product.name }}
diff --git a/static/components/product-card/product-card.js b/static/components/product-card/product-card.js index 9e8490b..5e049df 100644 --- a/static/components/product-card/product-card.js +++ b/static/components/product-card/product-card.js @@ -4,7 +4,7 @@ async function productCard(path) { name: 'product-card', template, - props: ['product', 'change-page'], + props: ['product', 'change-page', 'add-to-cart', 'is-stall'], data: function () { return {} }, diff --git a/static/components/product-detail/product-detail.html b/static/components/product-detail/product-detail.html index 92fa1c0..be31eda 100644 --- a/static/components/product-detail/product-detail.html +++ b/static/components/product-detail/product-detail.html @@ -17,6 +17,11 @@ style="/*background-size: contain; background-repeat: no-repeat*/" > +
@@ -47,7 +52,7 @@ {{ product.amount > 0 ? 'In stock.' : 'Out of stock.' }}{{ product.quantity > 0 ? 'In stock.' : 'Out of stock.' }}
@@ -56,12 +61,13 @@ color="primary" icon="shopping_cart" label="Add to cart" + @click="$emit('add-to-cart', product)" />
diff --git a/static/components/product-detail/product-detail.js b/static/components/product-detail/product-detail.js index 7b60f6b..d55b653 100644 --- a/static/components/product-detail/product-detail.js +++ b/static/components/product-detail/product-detail.js @@ -4,23 +4,14 @@ async function productDetail(path) { name: 'product-detail', template, - props: ['product'], + props: ['product', 'add-to-cart'], data: function () { return { slide: 1 } }, - computed: { - win_width() { - return this.$q.screen.width - 59 - }, - win_height() { - return this.$q.screen.height - 0 - } - }, + computed: {}, methods: {}, - created() { - console.log('ping') - } + created() {} }) } diff --git a/static/components/shopping-cart/shopping-cart.html b/static/components/shopping-cart/shopping-cart.html index 8650cc6..2864cf3 100644 --- a/static/components/shopping-cart/shopping-cart.html +++ b/static/components/shopping-cart/shopping-cart.html @@ -1 +1,51 @@ - + + + {{ cart.size }} + + + + + + {{p.quantity}} x + + + + + + + + + {{ p.name }} + + + + + {{p.currency != 'sat' ? p.formatedPrice : p.price + 'sats'}} + + + + + + +
+ +
diff --git a/static/components/shopping-cart/shopping-cart.js b/static/components/shopping-cart/shopping-cart.js index e0ad053..8f6902d 100644 --- a/static/components/shopping-cart/shopping-cart.js +++ b/static/components/shopping-cart/shopping-cart.js @@ -5,7 +5,7 @@ async function shoppingCart(path) { name: 'shopping-cart', template, - props: [], + props: ['cart', 'cart-menu', 'remove-from-cart', 'reset-cart'], data: function () { return {} }, diff --git a/static/js/market.js b/static/js/market.js index 6a0d723..95bf5a6 100644 --- a/static/js/market.js +++ b/static/js/market.js @@ -1,20 +1,13 @@ const market = async () => { Vue.component(VueQrcode.name, VueQrcode) - const nostr = window.NostrTools + const NostrTools = window.NostrTools const defaultRelays = [ 'wss://relay.damus.io', 'wss://relay.snort.social', - 'wss://nos.lol', 'wss://nostr.wine', - 'wss://relay.nostr.bg', 'wss://nostr-pub.wellorder.net', - 'wss://nostr-pub.semisol.dev', - 'wss://eden.nostr.land', - 'wss://nostr.mom', - 'wss://nostr.fmt.wiz.biz', - 'wss://nostr.zebedee.cloud', - 'wss://nostr.rocks' + 'wss://nostr.zebedee.cloud' ] const eventToObj = event => { event.content = JSON.parse(event.content) @@ -128,7 +121,7 @@ const market = async () => { }, methods: { naddr() { - let naddr = nostr.nip19.naddrEncode({ + let naddr = NostrTools.nip19.naddrEncode({ identifier: '1234', pubkey: 'c1415f950a1e3431de2bc5ee35144639e2f514cf158279abff9ed77d50118796', @@ -139,7 +132,7 @@ const market = async () => { }, async initNostr() { this.$q.loading.show() - const pool = new nostr.SimplePool() + const pool = new NostrTools.SimplePool() let relays = Array.from(this.relays) let products = new Map() let stalls = new Map() @@ -243,7 +236,7 @@ const market = async () => { let regExp = /^#([0-9a-f]{3}){1,2}$/i if (pubkey.startsWith('n')) { try { - let {type, data} = nostr.nip19.decode(pubkey) + let {type, data} = NostrTools.nip19.decode(pubkey) if (type === 'npub') pubkey = data else if (type === 'nprofile') { pubkey = data.pubkey diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html index c885e10..5f01c41 100644 --- a/templates/nostrmarket/market.html +++ b/templates/nostrmarket/market.html @@ -145,6 +145,7 @@ :products="filterProducts" :exchange-rates="exchangeRates" :product-detail="activeProduct" + :relays="relays" @change-page="navigateTo" >
-{% endblock %} {% block scripts %} - - -{% endblock %} From ec1428cf6aff44d24fc882ce5bf256426ee2d4b2 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Sun, 5 Mar 2023 20:32:35 +0000 Subject: [PATCH 137/891] ordering working --- .../customer-stall/customer-stall.js | 138 +++++++++++------- static/js/market.js | 2 +- 2 files changed, 89 insertions(+), 51 deletions(-) diff --git a/static/components/customer-stall/customer-stall.js b/static/components/customer-stall/customer-stall.js index 858ed9d..ce2be6e 100644 --- a/static/components/customer-stall/customer-stall.js +++ b/static/components/customer-stall/customer-stall.js @@ -24,9 +24,7 @@ async function customerStall(path) { hasNip07: false, checkoutDialog: { show: false, - data: { - pubkey: null - } + data: {} }, qrCodeDialog: { data: { @@ -130,56 +128,96 @@ async function customerStall(path) { this.checkoutDialog.data.pubkey = pk this.checkoutDialog.data.privkey = sk }, - placeOrder() { - LNbits.utils - .confirmDialog( - `Send the order to the merchant? You should receive a message with the payment details.` - ) - .onOk(async () => { - let orderData = this.checkoutDialog.data - let content = { - name: orderData?.username, - description: null, - address: orderData.address, - message: null, - contact: { - nostr: orderData.pubkey, - phone: null, - email: orderData?.email - }, - items: Array.from(this.cart.products, p => { - return {product_id: p[0], quantity: p[1].quantity} - }) - } - let event = { - kind: 4, - created_at: Math.floor(Date.now() / 1000), - tags: [], - content: await window.nostr.nip04.encrypt( - orderData.pubkey, - content - ), - pubkey: orderData.pubkey - } - event.id = NostrTools.getEventHash(event) - if (orderData.privkey) { - event.sig = NostrTools.signEvent(event, orderData.privkey) - } else if (this.hasNip07) { - await window.nostr.signEvent(event) - } - await this.sendOrder(event) + async placeOrder() { + // LNbits.utils + // .confirmDialog( + // `Send the order to the merchant? You should receive a message with the payment details.` + // ) + // .onOk(async () => { + let orderData = this.checkoutDialog.data + let orderObj = { + name: orderData?.username, + description: null, + address: orderData.address, + message: null, + contact: { + nostr: orderData.pubkey, + phone: null, + email: orderData?.email + }, + items: Array.from(this.cart.products, p => { + return {product_id: p[0], quantity: p[1].quantity} }) + } + let event = { + ...(await NostrTools.getBlankEvent()), + kind: 4, + created_at: Math.floor(Date.now() / 1000), + tags: [['p', this.stall.pubkey]], + pubkey: orderData.pubkey + } + if (orderData.privkey) { + event.content = await NostrTools.nip04.encrypt( + orderData.privkey, + this.stall.pubkey, + JSON.stringify(orderObj) + ) + } else { + console.log('use extension') + event.content = await window.nostr.nip04.encrypt( + orderData.pubkey, + JSON.stringify(orderObj) + ) + let userRelays = Object.keys( + (await window.nostr?.getRelays?.()) || [] + ) + if (userRelays.length != 0) { + userRelays.map(r => this.relays.add(r)) + } + } + event.id = NostrTools.getEventHash(event) + if (orderData.privkey) { + event.sig = await NostrTools.signEvent(event, orderData.privkey) + } else if (this.hasNip07) { + event = await window.nostr.signEvent(event) + } + console.log(event, orderData) + await this.sendOrder(event) + // }) }, async sendOrder(order) { - const pool = new NostrTools.SimplePool() - let relays = Array.from(this.relays) - let pubs = await pool.publish(relays, order) - pubs.on('ok', relay => { - console.log(`${relay.url} has accepted our event`) - }) - pubs.on('failed', reason => { - console.log(`failed to publish to ${reason}`) - }) + for (const url of Array.from(this.relays)) { + let relay = NostrTools.relayInit(url) + relay.on('connect', () => { + console.log(`connected to ${relay.url}`) + }) + relay.on('error', () => { + console.log(`failed to connect to ${relay.url}`) + }) + + await relay.connect() + let pub = relay.publish(order) + pub.on('ok', () => { + console.log(`${relay.url} has accepted our event`) + }) + pub.on('failed', reason => { + console.log(`failed to publish to ${relay.url}: ${reason}`) + }) + } + this.checkoutDialog = {show: false, data: {}} + // const pool = new NostrTools.SimplePool() + // let relays = Array.from(this.relays) + // try { + // let pubs = await pool.publish(relays, order) + // pubs.on('ok', relay => { + // console.log(`${relay.url} has accepted our event`) + // }) + // pubs.on('failed', (reason, err) => { + // console.log(`failed to publish to ${reason}: ${err}`) + // }) + // } catch (err) { + // console.error(err) + // } } }, created() { diff --git a/static/js/market.js b/static/js/market.js index 95bf5a6..a7c724e 100644 --- a/static/js/market.js +++ b/static/js/market.js @@ -5,7 +5,6 @@ const market = async () => { const defaultRelays = [ 'wss://relay.damus.io', 'wss://relay.snort.social', - 'wss://nostr.wine', 'wss://nostr-pub.wellorder.net', 'wss://nostr.zebedee.cloud' ] @@ -175,6 +174,7 @@ const market = async () => { return obj }) pool.close(relays) + return }, async getRates() { let noFiat = this.stalls.map(s => s.currency).every(c => c == 'sat') From 7da07491595f702e8960c72a7b85bf74d8cdf30b Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Sun, 5 Mar 2023 20:32:35 +0000 Subject: [PATCH 138/891] ordering working --- .../customer-stall/customer-stall.js | 138 +++++++++++------- static/js/market.js | 2 +- 2 files changed, 89 insertions(+), 51 deletions(-) diff --git a/static/components/customer-stall/customer-stall.js b/static/components/customer-stall/customer-stall.js index 858ed9d..ce2be6e 100644 --- a/static/components/customer-stall/customer-stall.js +++ b/static/components/customer-stall/customer-stall.js @@ -24,9 +24,7 @@ async function customerStall(path) { hasNip07: false, checkoutDialog: { show: false, - data: { - pubkey: null - } + data: {} }, qrCodeDialog: { data: { @@ -130,56 +128,96 @@ async function customerStall(path) { this.checkoutDialog.data.pubkey = pk this.checkoutDialog.data.privkey = sk }, - placeOrder() { - LNbits.utils - .confirmDialog( - `Send the order to the merchant? You should receive a message with the payment details.` - ) - .onOk(async () => { - let orderData = this.checkoutDialog.data - let content = { - name: orderData?.username, - description: null, - address: orderData.address, - message: null, - contact: { - nostr: orderData.pubkey, - phone: null, - email: orderData?.email - }, - items: Array.from(this.cart.products, p => { - return {product_id: p[0], quantity: p[1].quantity} - }) - } - let event = { - kind: 4, - created_at: Math.floor(Date.now() / 1000), - tags: [], - content: await window.nostr.nip04.encrypt( - orderData.pubkey, - content - ), - pubkey: orderData.pubkey - } - event.id = NostrTools.getEventHash(event) - if (orderData.privkey) { - event.sig = NostrTools.signEvent(event, orderData.privkey) - } else if (this.hasNip07) { - await window.nostr.signEvent(event) - } - await this.sendOrder(event) + async placeOrder() { + // LNbits.utils + // .confirmDialog( + // `Send the order to the merchant? You should receive a message with the payment details.` + // ) + // .onOk(async () => { + let orderData = this.checkoutDialog.data + let orderObj = { + name: orderData?.username, + description: null, + address: orderData.address, + message: null, + contact: { + nostr: orderData.pubkey, + phone: null, + email: orderData?.email + }, + items: Array.from(this.cart.products, p => { + return {product_id: p[0], quantity: p[1].quantity} }) + } + let event = { + ...(await NostrTools.getBlankEvent()), + kind: 4, + created_at: Math.floor(Date.now() / 1000), + tags: [['p', this.stall.pubkey]], + pubkey: orderData.pubkey + } + if (orderData.privkey) { + event.content = await NostrTools.nip04.encrypt( + orderData.privkey, + this.stall.pubkey, + JSON.stringify(orderObj) + ) + } else { + console.log('use extension') + event.content = await window.nostr.nip04.encrypt( + orderData.pubkey, + JSON.stringify(orderObj) + ) + let userRelays = Object.keys( + (await window.nostr?.getRelays?.()) || [] + ) + if (userRelays.length != 0) { + userRelays.map(r => this.relays.add(r)) + } + } + event.id = NostrTools.getEventHash(event) + if (orderData.privkey) { + event.sig = await NostrTools.signEvent(event, orderData.privkey) + } else if (this.hasNip07) { + event = await window.nostr.signEvent(event) + } + console.log(event, orderData) + await this.sendOrder(event) + // }) }, async sendOrder(order) { - const pool = new NostrTools.SimplePool() - let relays = Array.from(this.relays) - let pubs = await pool.publish(relays, order) - pubs.on('ok', relay => { - console.log(`${relay.url} has accepted our event`) - }) - pubs.on('failed', reason => { - console.log(`failed to publish to ${reason}`) - }) + for (const url of Array.from(this.relays)) { + let relay = NostrTools.relayInit(url) + relay.on('connect', () => { + console.log(`connected to ${relay.url}`) + }) + relay.on('error', () => { + console.log(`failed to connect to ${relay.url}`) + }) + + await relay.connect() + let pub = relay.publish(order) + pub.on('ok', () => { + console.log(`${relay.url} has accepted our event`) + }) + pub.on('failed', reason => { + console.log(`failed to publish to ${relay.url}: ${reason}`) + }) + } + this.checkoutDialog = {show: false, data: {}} + // const pool = new NostrTools.SimplePool() + // let relays = Array.from(this.relays) + // try { + // let pubs = await pool.publish(relays, order) + // pubs.on('ok', relay => { + // console.log(`${relay.url} has accepted our event`) + // }) + // pubs.on('failed', (reason, err) => { + // console.log(`failed to publish to ${reason}: ${err}`) + // }) + // } catch (err) { + // console.error(err) + // } } }, created() { diff --git a/static/js/market.js b/static/js/market.js index 95bf5a6..a7c724e 100644 --- a/static/js/market.js +++ b/static/js/market.js @@ -5,7 +5,6 @@ const market = async () => { const defaultRelays = [ 'wss://relay.damus.io', 'wss://relay.snort.social', - 'wss://nostr.wine', 'wss://nostr-pub.wellorder.net', 'wss://nostr.zebedee.cloud' ] @@ -175,6 +174,7 @@ const market = async () => { return obj }) pool.close(relays) + return }, async getRates() { let noFiat = this.stalls.map(s => s.currency).every(c => c == 'sat') From a2e8c66f87f1b15da0c9b776cd42ca79b1dc40e4 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Mon, 6 Mar 2023 09:59:17 +0000 Subject: [PATCH 139/891] hash string --- static/js/utils.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/static/js/utils.js b/static/js/utils.js index 11ebc81..e684ab8 100644 --- a/static/js/utils.js +++ b/static/js/utils.js @@ -16,3 +16,13 @@ function loadTemplateAsync(path) { return result } + +async function hash(string) { + const utf8 = new TextEncoder().encode(string) + const hashBuffer = await crypto.subtle.digest('SHA-256', utf8) + const hashArray = Array.from(new Uint8Array(hashBuffer)) + const hashHex = hashArray + .map(bytes => bytes.toString(16).padStart(2, '0')) + .join('') + return hashHex +} From abd169e7930cb1e5cdf230d0bb3e7c95594b6672 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Mon, 6 Mar 2023 09:59:17 +0000 Subject: [PATCH 140/891] hash string --- static/js/utils.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/static/js/utils.js b/static/js/utils.js index 11ebc81..e684ab8 100644 --- a/static/js/utils.js +++ b/static/js/utils.js @@ -16,3 +16,13 @@ function loadTemplateAsync(path) { return result } + +async function hash(string) { + const utf8 = new TextEncoder().encode(string) + const hashBuffer = await crypto.subtle.digest('SHA-256', utf8) + const hashArray = Array.from(new Uint8Array(hashBuffer)) + const hashHex = hashArray + .map(bytes => bytes.toString(16).padStart(2, '0')) + .join('') + return hashHex +} From dbaa9b9476ffbad901e7b35ab54503cf88266c58 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Mon, 6 Mar 2023 12:03:32 +0000 Subject: [PATCH 141/891] send/receive messages --- .../customer-stall/customer-stall.html | 7 + .../customer-stall/customer-stall.js | 239 +++++++++++------- 2 files changed, 159 insertions(+), 87 deletions(-) diff --git a/static/components/customer-stall/customer-stall.html b/static/components/customer-stall/customer-stall.html index aedec55..c6ac229 100644 --- a/static/components/customer-stall/customer-stall.html +++ b/static/components/customer-stall/customer-stall.html @@ -95,6 +95,13 @@ label="Email *optional" hint="Merchant may not use email" > +

Select the shipping zone:

{ - let orderData = this.checkoutDialog.data - let orderObj = { - name: orderData?.username, - description: null, - address: orderData.address, - message: null, - contact: { - nostr: orderData.pubkey, - phone: null, - email: orderData?.email - }, - items: Array.from(this.cart.products, p => { - return {product_id: p[0], quantity: p[1].quantity} + LNbits.utils + .confirmDialog( + `Send the order to the merchant? You should receive a message with the payment details.` + ) + .onOk(async () => { + let orderData = this.checkoutDialog.data + let orderObj = { + name: orderData?.username, + address: orderData.address, + message: orderData?.message, + contact: { + nostr: this.customerPubkey, + phone: null, + email: orderData?.email + }, + items: Array.from(this.cart.products, p => { + return {product_id: p[0], quantity: p[1].quantity} + }) + } + let created_at = Math.floor(Date.now() / 1000) + orderObj.id = await hash( + [this.customerPubkey, created_at, JSON.stringify(orderObj)].join( + ':' + ) + ) + let event = { + ...(await NostrTools.getBlankEvent()), + kind: 4, + created_at, + tags: [['p', this.stall.pubkey]], + pubkey: this.customerPubkey + } + if (this.customerPrivKey) { + event.content = await NostrTools.nip04.encrypt( + this.customerPrivKey, + this.stall.pubkey, + JSON.stringify(orderObj) + ) + } else { + event.content = await window.nostr.nip04.encrypt( + this.stall.pubkey, + JSON.stringify(orderObj) + ) + let userRelays = Object.keys( + (await window.nostr?.getRelays?.()) || [] + ) + if (userRelays.length != 0) { + userRelays.map(r => this.relays.add(r)) + } + } + event.id = NostrTools.getEventHash(event) + if (this.customerPrivKey) { + event.sig = await NostrTools.signEvent( + event, + this.customerPrivKey + ) + } else if (this.hasNip07) { + event = await window.nostr.signEvent(event) + } + console.log(event, orderObj) + await this.sendOrder(event) }) - } - let event = { - ...(await NostrTools.getBlankEvent()), - kind: 4, - created_at: Math.floor(Date.now() / 1000), - tags: [['p', this.stall.pubkey]], - pubkey: orderData.pubkey - } - if (orderData.privkey) { - event.content = await NostrTools.nip04.encrypt( - orderData.privkey, - this.stall.pubkey, - JSON.stringify(orderObj) - ) - } else { - console.log('use extension') - event.content = await window.nostr.nip04.encrypt( - orderData.pubkey, - JSON.stringify(orderObj) - ) - let userRelays = Object.keys( - (await window.nostr?.getRelays?.()) || [] - ) - if (userRelays.length != 0) { - userRelays.map(r => this.relays.add(r)) - } - } - event.id = NostrTools.getEventHash(event) - if (orderData.privkey) { - event.sig = await NostrTools.signEvent(event, orderData.privkey) - } else if (this.hasNip07) { - event = await window.nostr.signEvent(event) - } - console.log(event, orderData) - await this.sendOrder(event) - // }) }, async sendOrder(order) { for (const url of Array.from(this.relays)) { - let relay = NostrTools.relayInit(url) - relay.on('connect', () => { - console.log(`connected to ${relay.url}`) - }) - relay.on('error', () => { - console.log(`failed to connect to ${relay.url}`) - }) + try { + let relay = NostrTools.relayInit(url) + relay.on('connect', () => { + console.log(`connected to ${relay.url}`) + }) + relay.on('error', () => { + console.log(`failed to connect to ${relay.url}`) + }) - await relay.connect() - let pub = relay.publish(order) - pub.on('ok', () => { - console.log(`${relay.url} has accepted our event`) - }) - pub.on('failed', reason => { - console.log(`failed to publish to ${relay.url}: ${reason}`) - }) + await relay.connect() + let pub = relay.publish(order) + pub.on('ok', () => { + console.log(`${relay.url} has accepted our event`) + }) + pub.on('failed', reason => { + console.log(`failed to publish to ${relay.url}: ${reason}`) + }) + } catch (err) { + console.error(`Error: ${err}`) + } + } + this.resetCheckout() + this.listenMessages() + }, + async listenMessages() { + try { + const pool = new NostrTools.SimplePool() + const filters = [ + { + kinds: [4], + authors: [this.customerPubkey] + }, + { + kinds: [4], + '#p': [this.customerPubkey] + } + ] + let relays = Array.from(this.relays) + let subs = pool.sub(relays, filters) + subs.on('event', async event => { + let mine = event.pubkey == this.customerPubkey + let sender = mine + ? event.tags.find(([k, v]) => k === 'p' && v && v !== '')[1] + : event.pubkey + if ( + (mine && sender != this.stall.pubkey) || + (!mine && sender != this.customerPubkey) + ) { + console.log(`Not relevant message!`) + return + } + try { + let plaintext = this.customerPrivKey + ? await NostrTools.nip04.decrypt( + this.customerPrivKey, + sender, + event.content + ) + : await window.nostr.nip04.decrypt(sender, event.content) + // console.log(`${mine ? 'Me' : 'Customer'}: ${plaintext}`) + this.nostrMessages.set(event.id, { + msg: plaintext, + timestamp: event.created_at, + sender: `${mine ? 'Me' : 'Merchant'}` + }) + } catch { + console.error('Unable to decrypt message!') + return + } + }) + } catch (err) { + console.error(`Error: ${err}`) } - this.checkoutDialog = {show: false, data: {}} - // const pool = new NostrTools.SimplePool() - // let relays = Array.from(this.relays) - // try { - // let pubs = await pool.publish(relays, order) - // pubs.on('ok', relay => { - // console.log(`${relay.url} has accepted our event`) - // }) - // pubs.on('failed', (reason, err) => { - // console.log(`failed to publish to ${reason}: ${err}`) - // }) - // } catch (err) { - // console.error(err) - // } } }, created() { From 3f4860e1c05b7b4475dbf37c6cd372a39de96384 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Mon, 6 Mar 2023 12:03:32 +0000 Subject: [PATCH 142/891] send/receive messages --- .../customer-stall/customer-stall.html | 7 + .../customer-stall/customer-stall.js | 239 +++++++++++------- 2 files changed, 159 insertions(+), 87 deletions(-) diff --git a/static/components/customer-stall/customer-stall.html b/static/components/customer-stall/customer-stall.html index aedec55..c6ac229 100644 --- a/static/components/customer-stall/customer-stall.html +++ b/static/components/customer-stall/customer-stall.html @@ -95,6 +95,13 @@ label="Email *optional" hint="Merchant may not use email" > +

Select the shipping zone:

{ - let orderData = this.checkoutDialog.data - let orderObj = { - name: orderData?.username, - description: null, - address: orderData.address, - message: null, - contact: { - nostr: orderData.pubkey, - phone: null, - email: orderData?.email - }, - items: Array.from(this.cart.products, p => { - return {product_id: p[0], quantity: p[1].quantity} + LNbits.utils + .confirmDialog( + `Send the order to the merchant? You should receive a message with the payment details.` + ) + .onOk(async () => { + let orderData = this.checkoutDialog.data + let orderObj = { + name: orderData?.username, + address: orderData.address, + message: orderData?.message, + contact: { + nostr: this.customerPubkey, + phone: null, + email: orderData?.email + }, + items: Array.from(this.cart.products, p => { + return {product_id: p[0], quantity: p[1].quantity} + }) + } + let created_at = Math.floor(Date.now() / 1000) + orderObj.id = await hash( + [this.customerPubkey, created_at, JSON.stringify(orderObj)].join( + ':' + ) + ) + let event = { + ...(await NostrTools.getBlankEvent()), + kind: 4, + created_at, + tags: [['p', this.stall.pubkey]], + pubkey: this.customerPubkey + } + if (this.customerPrivKey) { + event.content = await NostrTools.nip04.encrypt( + this.customerPrivKey, + this.stall.pubkey, + JSON.stringify(orderObj) + ) + } else { + event.content = await window.nostr.nip04.encrypt( + this.stall.pubkey, + JSON.stringify(orderObj) + ) + let userRelays = Object.keys( + (await window.nostr?.getRelays?.()) || [] + ) + if (userRelays.length != 0) { + userRelays.map(r => this.relays.add(r)) + } + } + event.id = NostrTools.getEventHash(event) + if (this.customerPrivKey) { + event.sig = await NostrTools.signEvent( + event, + this.customerPrivKey + ) + } else if (this.hasNip07) { + event = await window.nostr.signEvent(event) + } + console.log(event, orderObj) + await this.sendOrder(event) }) - } - let event = { - ...(await NostrTools.getBlankEvent()), - kind: 4, - created_at: Math.floor(Date.now() / 1000), - tags: [['p', this.stall.pubkey]], - pubkey: orderData.pubkey - } - if (orderData.privkey) { - event.content = await NostrTools.nip04.encrypt( - orderData.privkey, - this.stall.pubkey, - JSON.stringify(orderObj) - ) - } else { - console.log('use extension') - event.content = await window.nostr.nip04.encrypt( - orderData.pubkey, - JSON.stringify(orderObj) - ) - let userRelays = Object.keys( - (await window.nostr?.getRelays?.()) || [] - ) - if (userRelays.length != 0) { - userRelays.map(r => this.relays.add(r)) - } - } - event.id = NostrTools.getEventHash(event) - if (orderData.privkey) { - event.sig = await NostrTools.signEvent(event, orderData.privkey) - } else if (this.hasNip07) { - event = await window.nostr.signEvent(event) - } - console.log(event, orderData) - await this.sendOrder(event) - // }) }, async sendOrder(order) { for (const url of Array.from(this.relays)) { - let relay = NostrTools.relayInit(url) - relay.on('connect', () => { - console.log(`connected to ${relay.url}`) - }) - relay.on('error', () => { - console.log(`failed to connect to ${relay.url}`) - }) + try { + let relay = NostrTools.relayInit(url) + relay.on('connect', () => { + console.log(`connected to ${relay.url}`) + }) + relay.on('error', () => { + console.log(`failed to connect to ${relay.url}`) + }) - await relay.connect() - let pub = relay.publish(order) - pub.on('ok', () => { - console.log(`${relay.url} has accepted our event`) - }) - pub.on('failed', reason => { - console.log(`failed to publish to ${relay.url}: ${reason}`) - }) + await relay.connect() + let pub = relay.publish(order) + pub.on('ok', () => { + console.log(`${relay.url} has accepted our event`) + }) + pub.on('failed', reason => { + console.log(`failed to publish to ${relay.url}: ${reason}`) + }) + } catch (err) { + console.error(`Error: ${err}`) + } + } + this.resetCheckout() + this.listenMessages() + }, + async listenMessages() { + try { + const pool = new NostrTools.SimplePool() + const filters = [ + { + kinds: [4], + authors: [this.customerPubkey] + }, + { + kinds: [4], + '#p': [this.customerPubkey] + } + ] + let relays = Array.from(this.relays) + let subs = pool.sub(relays, filters) + subs.on('event', async event => { + let mine = event.pubkey == this.customerPubkey + let sender = mine + ? event.tags.find(([k, v]) => k === 'p' && v && v !== '')[1] + : event.pubkey + if ( + (mine && sender != this.stall.pubkey) || + (!mine && sender != this.customerPubkey) + ) { + console.log(`Not relevant message!`) + return + } + try { + let plaintext = this.customerPrivKey + ? await NostrTools.nip04.decrypt( + this.customerPrivKey, + sender, + event.content + ) + : await window.nostr.nip04.decrypt(sender, event.content) + // console.log(`${mine ? 'Me' : 'Customer'}: ${plaintext}`) + this.nostrMessages.set(event.id, { + msg: plaintext, + timestamp: event.created_at, + sender: `${mine ? 'Me' : 'Merchant'}` + }) + } catch { + console.error('Unable to decrypt message!') + return + } + }) + } catch (err) { + console.error(`Error: ${err}`) } - this.checkoutDialog = {show: false, data: {}} - // const pool = new NostrTools.SimplePool() - // let relays = Array.from(this.relays) - // try { - // let pubs = await pool.publish(relays, order) - // pubs.on('ok', relay => { - // console.log(`${relay.url} has accepted our event`) - // }) - // pubs.on('failed', (reason, err) => { - // console.log(`failed to publish to ${reason}: ${err}`) - // }) - // } catch (err) { - // console.error(err) - // } } }, created() { From d0471744e01bf8cbc3ce4879947277fb410214e4 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Mon, 6 Mar 2023 15:03:02 +0200 Subject: [PATCH 143/891] feat: create order on DM --- __init__.py | 21 ++++++++++++--- crud.py | 63 +++++++++++++++++++++++++++++++++++++++++-- helpers.py | 12 ++++----- migrations.py | 41 +++++----------------------- models.py | 37 +++++++++++++++++++++++++ nostr/nostr_client.py | 30 ++++++++++----------- tasks.py | 61 ++++++++++++++++++++++++++++++++++------- views_api.py | 52 ++++++++++++++++++++++++++++++++++- 8 files changed, 246 insertions(+), 71 deletions(-) diff --git a/__init__.py b/__init__.py index 9f3a39e..b4f7af0 100644 --- a/__init__.py +++ b/__init__.py @@ -1,5 +1,5 @@ import asyncio -from asyncio import Task +from asyncio import Queue, Task from typing import List from fastapi import APIRouter @@ -26,16 +26,29 @@ def nostrmarket_renderer(): return template_renderer(["lnbits/extensions/nostrmarket/templates"]) +recieve_event_queue: Queue = Queue() +send_req_queue: Queue = Queue() scheduled_tasks: List[Task] = [] -from .tasks import subscribe_nostrclient, wait_for_nostr_events, wait_for_paid_invoices + +from .tasks import ( + subscribe_to_nostr_client, + wait_for_nostr_events, + wait_for_paid_invoices, +) from .views import * # noqa from .views_api import * # noqa def nostrmarket_start(): + async def _subscribe_to_nostr_client(): + await subscribe_to_nostr_client(recieve_event_queue, send_req_queue) + + async def _wait_for_nostr_events(): + await wait_for_nostr_events(recieve_event_queue, send_req_queue) + loop = asyncio.get_event_loop() task1 = loop.create_task(catch_everything_and_restart(wait_for_paid_invoices)) - task2 = loop.create_task(catch_everything_and_restart(subscribe_nostrclient)) - task3 = loop.create_task(catch_everything_and_restart(wait_for_nostr_events)) + task2 = loop.create_task(catch_everything_and_restart(_subscribe_to_nostr_client)) + task3 = loop.create_task(catch_everything_and_restart(_wait_for_nostr_events)) scheduled_tasks.append([task1, task2, task3]) diff --git a/crud.py b/crud.py index edd9a09..d443dd0 100644 --- a/crud.py +++ b/crud.py @@ -7,7 +7,9 @@ from lnbits.helpers import urlsafe_short_hash from . import db from .models import ( Merchant, + Order, PartialMerchant, + PartialOrder, PartialProduct, PartialStall, PartialZone, @@ -206,7 +208,7 @@ async def delete_stall(user_id: str, stall_id: str) -> None: ) -######################################## STALL ######################################## +######################################## PRODUCTS ######################################## async def create_product(user_id: str, data: PartialProduct) -> Product: @@ -214,7 +216,7 @@ async def create_product(user_id: str, data: PartialProduct) -> Product: await db.execute( f""" - INSERT INTO nostrmarket.products (user_id, id, stall_id, name, images, price, quantity, category_list, meta) + INSERT INTO nostrmarket.products (user_id, id, stall_id, name, image, price, quantity, category_list, meta) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( @@ -278,6 +280,29 @@ async def get_products(user_id: str, stall_id: str) -> List[Product]: return [Product.from_row(row) for row in rows] +async def get_products_by_ids(user_id: str, product_ids: List[str]) -> List[Product]: + q = ",".join(["?"] * len(product_ids)) + rows = await db.fetchall( + f"SELECT id, stall_id, name, price, quantity FROM nostrmarket.products WHERE user_id = ? AND id IN ({q})", + (user_id, *product_ids), + ) + return [Product.from_row(row) for row in rows] + + + +async def get_wallet_for_product(product_id: str) -> Optional[str]: + row = await db.fetchone( + """ + SELECT s.wallet FROM nostrmarket.products p + INNER JOIN nostrmarket.stalls s + ON p.stall_id = s.id + WHERE p.id=? + """, + (product_id,), + ) + return row[0] if row else None + + async def delete_product(user_id: str, product_id: str) -> None: await db.execute( "DELETE FROM nostrmarket.products WHERE user_id =? AND id = ?", @@ -286,3 +311,37 @@ async def delete_product(user_id: str, product_id: str) -> None: product_id, ), ) + +######################################## ORDERS ######################################## + +async def create_order(user_id: str, o: Order) -> Order: + await db.execute( + f""" + INSERT INTO nostrmarket.orders (user_id, id, event_id, pubkey, contact_data, order_items, invoice_id, total) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + """, + ( + user_id, + o.id, + o.event_id, + o.pubkey, + json.dumps(o.contact.dict()), + json.dumps([i.dict() for i in o.items]), + o.invoice_id, + o.total, + ), + ) + order = await get_order(user_id, o.id) + assert order, "Newly created order couldn't be retrieved" + + return order + +async def get_order(user_id: str, order_id: str) -> Optional[Order]: + row = await db.fetchone( + "SELECT * FROM nostrmarket.orders WHERE user_id =? AND id = ?", + ( + user_id, + order_id, + ), + ) + return Order.from_row(row) if row else None diff --git a/helpers.py b/helpers.py index 8747c48..5141009 100644 --- a/helpers.py +++ b/helpers.py @@ -1,7 +1,7 @@ import base64 import json import secrets -from typing import Optional +from typing import Any, Optional, Tuple import secp256k1 from cffi import FFI @@ -73,9 +73,9 @@ def copy_x(output, x32, y32, data): return 1 -def is_json(string: str): +def order_from_json(s: str) -> Tuple[Optional[Any], Optional[str]]: try: - json.loads(string) - except ValueError as e: - return False - return True + order = json.loads(s) + return (order, None) if "items" in order else (None, s) + except ValueError: + return None, s diff --git a/migrations.py b/migrations.py index 680c3cc..eead2a7 100644 --- a/migrations.py +++ b/migrations.py @@ -71,39 +71,25 @@ async def m001_initial(db): """ Initial orders table. """ + empty_object = "{}" await db.execute( f""" CREATE TABLE nostrmarket.orders ( + user_id TEXT NOT NULL, id TEXT PRIMARY KEY, - wallet TEXT NOT NULL, - username TEXT, + event_id TEXT, pubkey TEXT, - shipping_zone TEXT NOT NULL, - address TEXT, - email TEXT, + contact_data TEXT NOT NULL DEFAULT '{empty_object}', + order_items TEXT NOT NULL, total REAL NOT NULL, invoice_id TEXT NOT NULL, - paid BOOLEAN NOT NULL, - shipped BOOLEAN NOT NULL, + paid BOOLEAN NOT NULL DEFAULT false, + shipped BOOLEAN NOT NULL DEFAULT false, time TIMESTAMP NOT NULL DEFAULT {db.timestamp_now} ); """ ) - """ - Initial order details table. - """ - await db.execute( - f""" - CREATE TABLE nostrmarket.order_details ( - id TEXT PRIMARY KEY, - order_id TEXT NOT NULL, - product_id TEXT NOT NULL, - quantity INTEGER NOT NULL - ); - """ - ) - """ Initial market table. """ @@ -117,19 +103,6 @@ async def m001_initial(db): """ ) - """ - Initial market stalls table. - """ - await db.execute( - f""" - CREATE TABLE nostrmarket.market_stalls ( - id TEXT PRIMARY KEY, - market_id TEXT NOT NULL, - stall_id TEXT NOT NULL - ); - """ - ) - """ Initial chat messages table. """ diff --git a/models.py b/models.py index 58b093d..2747085 100644 --- a/models.py +++ b/models.py @@ -217,3 +217,40 @@ class Product(PartialProduct, Nostrable): product.config = ProductConfig(**json.loads(row["meta"])) product.categories = json.loads(row["category_list"]) return product + + +######################################## ORDERS ######################################## + + +class OrderItem(BaseModel): + product_id: str + quantity: int + + +class OrderContact(BaseModel): + nostr: Optional[str] + phone: Optional[str] + email: Optional[str] + + +class PartialOrder(BaseModel): + id: Optional[str] + event_id: Optional[str] + pubkey: str + items: List[OrderItem] + contact: Optional[OrderContact] + + +class Order(PartialOrder): + id: str + invoice_id: str + total: float + paid: bool = False + shipped: bool = False + + @classmethod + def from_row(cls, row: Row) -> "Order": + contact = OrderContact(**json.loads(row["contact_data"])) + items = [OrderItem(**z) for z in json.loads(row["order_items"])] + order = cls(**dict(row), contact=contact, items=items) + return order \ No newline at end of file diff --git a/nostr/nostr_client.py b/nostr/nostr_client.py index bb64c58..3e8a47e 100644 --- a/nostr/nostr_client.py +++ b/nostr/nostr_client.py @@ -46,21 +46,21 @@ async def connect_to_nostrclient_ws( return ws -async def handle_event(event, pubkeys): - tags = [t[1] for t in event["tags"] if t[0] == "p"] - to_merchant = None - if tags and len(tags) > 0: - to_merchant = tags[0] +# async def handle_event(event, pubkeys): +# tags = [t[1] for t in event["tags"] if t[0] == "p"] +# to_merchant = None +# if tags and len(tags) > 0: +# to_merchant = tags[0] - if event["pubkey"] in pubkeys or to_merchant in pubkeys: - logger.debug(f"Event sent to {to_merchant}") - pubkey = to_merchant if to_merchant in pubkeys else event["pubkey"] - # Send event to market extension - await send_event_to_market(event=event, pubkey=pubkey) +# if event["pubkey"] in pubkeys or to_merchant in pubkeys: +# logger.debug(f"Event sent to {to_merchant}") +# pubkey = to_merchant if to_merchant in pubkeys else event["pubkey"] +# # Send event to market extension +# await send_event_to_market(event=event, pubkey=pubkey) -async def send_event_to_market(event: dict, pubkey: str): - # Sends event to market extension, for decrypt and handling - market_url = url_for(f"/market/api/v1/nip04/{pubkey}", external=True) - async with httpx.AsyncClient() as client: - await client.post(url=market_url, json=event) +# async def send_event_to_market(event: dict, pubkey: str): +# # Sends event to market extension, for decrypt and handling +# market_url = url_for(f"/market/api/v1/nip04/{pubkey}", external=True) +# async with httpx.AsyncClient() as client: +# await client.post(url=market_url, json=event) diff --git a/tasks.py b/tasks.py index 901504c..4ee1e4d 100644 --- a/tasks.py +++ b/tasks.py @@ -7,16 +7,22 @@ import websocket from loguru import logger from websocket import WebSocketApp +from lnbits.core import get_wallet from lnbits.core.models import Payment +from lnbits.extensions.nostrmarket.models import PartialOrder +from lnbits.helpers import url_for from lnbits.tasks import register_invoice_listener -from .crud import get_merchant, get_merchant_by_pubkey, get_public_keys_for_merchants +from .crud import ( + get_merchant_by_pubkey, + get_product, + get_public_keys_for_merchants, + get_wallet_for_product, +) +from .helpers import order_from_json from .nostr.event import NostrEvent from .nostr.nostr_client import connect_to_nostrclient_ws -recieve_event_queue: Queue = Queue() -send_req_queue: Queue = Queue() - async def wait_for_paid_invoices(): invoice_queue = Queue() @@ -34,7 +40,7 @@ async def on_invoice_paid(payment: Payment) -> None: print("### on_invoice_paid") -async def subscribe_nostrclient(): +async def subscribe_to_nostr_client(recieve_event_queue: Queue, send_req_queue: Queue): print("### subscribe_nostrclient_ws") def on_open(_): @@ -65,7 +71,7 @@ async def subscribe_nostrclient(): await asyncio.sleep(5) -async def wait_for_nostr_events(): +async def wait_for_nostr_events(recieve_event_queue: Queue, send_req_queue: Queue): public_keys = await get_public_keys_for_merchants() for p in public_keys: await send_req_queue.put( @@ -85,10 +91,47 @@ async def handle_message(msg: str): event = NostrEvent(**event) if event.kind == 4: merchant = await get_merchant_by_pubkey(public_key) - if not merchant: - return + assert merchant, f"Merchant not found for public key '{public_key}'" + clear_text_msg = merchant.decrypt_message(event.content, event.pubkey) - print("### clear_text_msg", clear_text_msg) + await handle_nip04_message( + event.pubkey, event.id, clear_text_msg + ) except Exception as ex: logger.warning(ex) + + +async def handle_nip04_message(from_pubkey: str, event_id: str, msg: str): + order, text_msg = order_from_json(msg) + try: + if order: + print("### order", from_pubkey, event_id, msg) + ### check that event_id not parsed already + order["pubkey"] = from_pubkey + order["event_id"] = event_id + partial_order = PartialOrder(**order) + assert len(partial_order.items) != 0, "Order has no items. Order: " + msg + + first_product_id = partial_order.items[0].product_id + wallet_id = await get_wallet_for_product(first_product_id) + assert ( + wallet_id + ), f"Cannot find wallet id for product id: {first_product_id}" + + wallet = await get_wallet(wallet_id) + assert wallet, f"Cannot find wallet for product id: {first_product_id}" + + market_url = url_for(f"/nostrmarket/api/v1/order", external=True) + async with httpx.AsyncClient() as client: + await client.post( + url=market_url, + headers={ + "X-Api-Key": wallet.adminkey, + }, + json=order, + ) + else: + print("### text_msg", text_msg) + except Exception as ex: + logger.warning(ex) diff --git a/views_api.py b/views_api.py index ecb14f9..2102dc9 100644 --- a/views_api.py +++ b/views_api.py @@ -5,6 +5,7 @@ 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, @@ -17,6 +18,7 @@ from lnbits.utils.exchange_rates import currencies from . import nostrmarket_ext from .crud import ( create_merchant, + create_order, create_product, create_stall, create_zone, @@ -26,8 +28,10 @@ from .crud import ( get_merchant_for_user, get_product, get_products, + get_products_by_ids, get_stall, get_stalls, + get_wallet_for_product, get_zone, get_zones, update_product, @@ -37,7 +41,9 @@ from .crud import ( from .models import ( Merchant, Nostrable, + Order, PartialMerchant, + PartialOrder, PartialProduct, PartialStall, PartialZone, @@ -101,7 +107,7 @@ async def api_get_zones(wallet: WalletTypeInfo = Depends(get_key_type)) -> List[ @nostrmarket_ext.post("/api/v1/zone") async def api_create_zone( - data: PartialZone, wallet: WalletTypeInfo = Depends(get_key_type) + data: PartialZone, wallet: WalletTypeInfo = Depends(require_admin_key) ): try: zone = await create_zone(wallet.wallet.user, data) @@ -418,6 +424,50 @@ async def api_delete_product( ) +######################################## ORDERS ######################################## + + +@nostrmarket_ext.post("/api/v1/order") +async def api_create_order( + data: PartialOrder, wallet: WalletTypeInfo = Depends(require_admin_key) +): + try: + data.id = data.id or data.event_id + + wallet_id = await get_wallet_for_product(data.items[0].product_id) + assert wallet_id, "Missing wallet for order `{data.id}`" + + product_ids = [p.product_id for p in data.items] + products = await get_products_by_ids(wallet.wallet.user, product_ids) + + product_prices = {} + for p in products: + product_prices[p.id] = p + + amount: float = 0 # todo + for item in data.items: + amount += item.quantity * product_prices[item.product_id].price + + payment_hash, payment_request = await create_invoice( + wallet_id=wallet_id, + amount=round(amount), + memo=f"Order '{data.id}' for pubkey '{data.pubkey}'", + extra={ + "tag": "nostrmarket", + "order_id": data.id, + } + ) + + order = Order(**data.dict(), invoice_id=payment_hash, total=100) + await create_order(wallet.wallet.user, order) + except Exception as ex: + logger.warning(ex) + raise HTTPException( + status_code=HTTPStatus.INTERNAL_SERVER_ERROR, + detail="Cannot create order", + ) + + ######################################## OTHER ######################################## From 2912589b70a29cc60740f3c6fad1b701f1d772ff Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Mon, 6 Mar 2023 15:03:02 +0200 Subject: [PATCH 144/891] feat: create order on DM --- __init__.py | 21 ++++++++++++--- crud.py | 63 +++++++++++++++++++++++++++++++++++++++++-- helpers.py | 12 ++++----- migrations.py | 41 +++++----------------------- models.py | 37 +++++++++++++++++++++++++ nostr/nostr_client.py | 30 ++++++++++----------- tasks.py | 61 ++++++++++++++++++++++++++++++++++------- views_api.py | 52 ++++++++++++++++++++++++++++++++++- 8 files changed, 246 insertions(+), 71 deletions(-) diff --git a/__init__.py b/__init__.py index 9f3a39e..b4f7af0 100644 --- a/__init__.py +++ b/__init__.py @@ -1,5 +1,5 @@ import asyncio -from asyncio import Task +from asyncio import Queue, Task from typing import List from fastapi import APIRouter @@ -26,16 +26,29 @@ def nostrmarket_renderer(): return template_renderer(["lnbits/extensions/nostrmarket/templates"]) +recieve_event_queue: Queue = Queue() +send_req_queue: Queue = Queue() scheduled_tasks: List[Task] = [] -from .tasks import subscribe_nostrclient, wait_for_nostr_events, wait_for_paid_invoices + +from .tasks import ( + subscribe_to_nostr_client, + wait_for_nostr_events, + wait_for_paid_invoices, +) from .views import * # noqa from .views_api import * # noqa def nostrmarket_start(): + async def _subscribe_to_nostr_client(): + await subscribe_to_nostr_client(recieve_event_queue, send_req_queue) + + async def _wait_for_nostr_events(): + await wait_for_nostr_events(recieve_event_queue, send_req_queue) + loop = asyncio.get_event_loop() task1 = loop.create_task(catch_everything_and_restart(wait_for_paid_invoices)) - task2 = loop.create_task(catch_everything_and_restart(subscribe_nostrclient)) - task3 = loop.create_task(catch_everything_and_restart(wait_for_nostr_events)) + task2 = loop.create_task(catch_everything_and_restart(_subscribe_to_nostr_client)) + task3 = loop.create_task(catch_everything_and_restart(_wait_for_nostr_events)) scheduled_tasks.append([task1, task2, task3]) diff --git a/crud.py b/crud.py index edd9a09..d443dd0 100644 --- a/crud.py +++ b/crud.py @@ -7,7 +7,9 @@ from lnbits.helpers import urlsafe_short_hash from . import db from .models import ( Merchant, + Order, PartialMerchant, + PartialOrder, PartialProduct, PartialStall, PartialZone, @@ -206,7 +208,7 @@ async def delete_stall(user_id: str, stall_id: str) -> None: ) -######################################## STALL ######################################## +######################################## PRODUCTS ######################################## async def create_product(user_id: str, data: PartialProduct) -> Product: @@ -214,7 +216,7 @@ async def create_product(user_id: str, data: PartialProduct) -> Product: await db.execute( f""" - INSERT INTO nostrmarket.products (user_id, id, stall_id, name, images, price, quantity, category_list, meta) + INSERT INTO nostrmarket.products (user_id, id, stall_id, name, image, price, quantity, category_list, meta) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( @@ -278,6 +280,29 @@ async def get_products(user_id: str, stall_id: str) -> List[Product]: return [Product.from_row(row) for row in rows] +async def get_products_by_ids(user_id: str, product_ids: List[str]) -> List[Product]: + q = ",".join(["?"] * len(product_ids)) + rows = await db.fetchall( + f"SELECT id, stall_id, name, price, quantity FROM nostrmarket.products WHERE user_id = ? AND id IN ({q})", + (user_id, *product_ids), + ) + return [Product.from_row(row) for row in rows] + + + +async def get_wallet_for_product(product_id: str) -> Optional[str]: + row = await db.fetchone( + """ + SELECT s.wallet FROM nostrmarket.products p + INNER JOIN nostrmarket.stalls s + ON p.stall_id = s.id + WHERE p.id=? + """, + (product_id,), + ) + return row[0] if row else None + + async def delete_product(user_id: str, product_id: str) -> None: await db.execute( "DELETE FROM nostrmarket.products WHERE user_id =? AND id = ?", @@ -286,3 +311,37 @@ async def delete_product(user_id: str, product_id: str) -> None: product_id, ), ) + +######################################## ORDERS ######################################## + +async def create_order(user_id: str, o: Order) -> Order: + await db.execute( + f""" + INSERT INTO nostrmarket.orders (user_id, id, event_id, pubkey, contact_data, order_items, invoice_id, total) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + """, + ( + user_id, + o.id, + o.event_id, + o.pubkey, + json.dumps(o.contact.dict()), + json.dumps([i.dict() for i in o.items]), + o.invoice_id, + o.total, + ), + ) + order = await get_order(user_id, o.id) + assert order, "Newly created order couldn't be retrieved" + + return order + +async def get_order(user_id: str, order_id: str) -> Optional[Order]: + row = await db.fetchone( + "SELECT * FROM nostrmarket.orders WHERE user_id =? AND id = ?", + ( + user_id, + order_id, + ), + ) + return Order.from_row(row) if row else None diff --git a/helpers.py b/helpers.py index 8747c48..5141009 100644 --- a/helpers.py +++ b/helpers.py @@ -1,7 +1,7 @@ import base64 import json import secrets -from typing import Optional +from typing import Any, Optional, Tuple import secp256k1 from cffi import FFI @@ -73,9 +73,9 @@ def copy_x(output, x32, y32, data): return 1 -def is_json(string: str): +def order_from_json(s: str) -> Tuple[Optional[Any], Optional[str]]: try: - json.loads(string) - except ValueError as e: - return False - return True + order = json.loads(s) + return (order, None) if "items" in order else (None, s) + except ValueError: + return None, s diff --git a/migrations.py b/migrations.py index 680c3cc..eead2a7 100644 --- a/migrations.py +++ b/migrations.py @@ -71,39 +71,25 @@ async def m001_initial(db): """ Initial orders table. """ + empty_object = "{}" await db.execute( f""" CREATE TABLE nostrmarket.orders ( + user_id TEXT NOT NULL, id TEXT PRIMARY KEY, - wallet TEXT NOT NULL, - username TEXT, + event_id TEXT, pubkey TEXT, - shipping_zone TEXT NOT NULL, - address TEXT, - email TEXT, + contact_data TEXT NOT NULL DEFAULT '{empty_object}', + order_items TEXT NOT NULL, total REAL NOT NULL, invoice_id TEXT NOT NULL, - paid BOOLEAN NOT NULL, - shipped BOOLEAN NOT NULL, + paid BOOLEAN NOT NULL DEFAULT false, + shipped BOOLEAN NOT NULL DEFAULT false, time TIMESTAMP NOT NULL DEFAULT {db.timestamp_now} ); """ ) - """ - Initial order details table. - """ - await db.execute( - f""" - CREATE TABLE nostrmarket.order_details ( - id TEXT PRIMARY KEY, - order_id TEXT NOT NULL, - product_id TEXT NOT NULL, - quantity INTEGER NOT NULL - ); - """ - ) - """ Initial market table. """ @@ -117,19 +103,6 @@ async def m001_initial(db): """ ) - """ - Initial market stalls table. - """ - await db.execute( - f""" - CREATE TABLE nostrmarket.market_stalls ( - id TEXT PRIMARY KEY, - market_id TEXT NOT NULL, - stall_id TEXT NOT NULL - ); - """ - ) - """ Initial chat messages table. """ diff --git a/models.py b/models.py index 58b093d..2747085 100644 --- a/models.py +++ b/models.py @@ -217,3 +217,40 @@ class Product(PartialProduct, Nostrable): product.config = ProductConfig(**json.loads(row["meta"])) product.categories = json.loads(row["category_list"]) return product + + +######################################## ORDERS ######################################## + + +class OrderItem(BaseModel): + product_id: str + quantity: int + + +class OrderContact(BaseModel): + nostr: Optional[str] + phone: Optional[str] + email: Optional[str] + + +class PartialOrder(BaseModel): + id: Optional[str] + event_id: Optional[str] + pubkey: str + items: List[OrderItem] + contact: Optional[OrderContact] + + +class Order(PartialOrder): + id: str + invoice_id: str + total: float + paid: bool = False + shipped: bool = False + + @classmethod + def from_row(cls, row: Row) -> "Order": + contact = OrderContact(**json.loads(row["contact_data"])) + items = [OrderItem(**z) for z in json.loads(row["order_items"])] + order = cls(**dict(row), contact=contact, items=items) + return order \ No newline at end of file diff --git a/nostr/nostr_client.py b/nostr/nostr_client.py index bb64c58..3e8a47e 100644 --- a/nostr/nostr_client.py +++ b/nostr/nostr_client.py @@ -46,21 +46,21 @@ async def connect_to_nostrclient_ws( return ws -async def handle_event(event, pubkeys): - tags = [t[1] for t in event["tags"] if t[0] == "p"] - to_merchant = None - if tags and len(tags) > 0: - to_merchant = tags[0] +# async def handle_event(event, pubkeys): +# tags = [t[1] for t in event["tags"] if t[0] == "p"] +# to_merchant = None +# if tags and len(tags) > 0: +# to_merchant = tags[0] - if event["pubkey"] in pubkeys or to_merchant in pubkeys: - logger.debug(f"Event sent to {to_merchant}") - pubkey = to_merchant if to_merchant in pubkeys else event["pubkey"] - # Send event to market extension - await send_event_to_market(event=event, pubkey=pubkey) +# if event["pubkey"] in pubkeys or to_merchant in pubkeys: +# logger.debug(f"Event sent to {to_merchant}") +# pubkey = to_merchant if to_merchant in pubkeys else event["pubkey"] +# # Send event to market extension +# await send_event_to_market(event=event, pubkey=pubkey) -async def send_event_to_market(event: dict, pubkey: str): - # Sends event to market extension, for decrypt and handling - market_url = url_for(f"/market/api/v1/nip04/{pubkey}", external=True) - async with httpx.AsyncClient() as client: - await client.post(url=market_url, json=event) +# async def send_event_to_market(event: dict, pubkey: str): +# # Sends event to market extension, for decrypt and handling +# market_url = url_for(f"/market/api/v1/nip04/{pubkey}", external=True) +# async with httpx.AsyncClient() as client: +# await client.post(url=market_url, json=event) diff --git a/tasks.py b/tasks.py index 901504c..4ee1e4d 100644 --- a/tasks.py +++ b/tasks.py @@ -7,16 +7,22 @@ import websocket from loguru import logger from websocket import WebSocketApp +from lnbits.core import get_wallet from lnbits.core.models import Payment +from lnbits.extensions.nostrmarket.models import PartialOrder +from lnbits.helpers import url_for from lnbits.tasks import register_invoice_listener -from .crud import get_merchant, get_merchant_by_pubkey, get_public_keys_for_merchants +from .crud import ( + get_merchant_by_pubkey, + get_product, + get_public_keys_for_merchants, + get_wallet_for_product, +) +from .helpers import order_from_json from .nostr.event import NostrEvent from .nostr.nostr_client import connect_to_nostrclient_ws -recieve_event_queue: Queue = Queue() -send_req_queue: Queue = Queue() - async def wait_for_paid_invoices(): invoice_queue = Queue() @@ -34,7 +40,7 @@ async def on_invoice_paid(payment: Payment) -> None: print("### on_invoice_paid") -async def subscribe_nostrclient(): +async def subscribe_to_nostr_client(recieve_event_queue: Queue, send_req_queue: Queue): print("### subscribe_nostrclient_ws") def on_open(_): @@ -65,7 +71,7 @@ async def subscribe_nostrclient(): await asyncio.sleep(5) -async def wait_for_nostr_events(): +async def wait_for_nostr_events(recieve_event_queue: Queue, send_req_queue: Queue): public_keys = await get_public_keys_for_merchants() for p in public_keys: await send_req_queue.put( @@ -85,10 +91,47 @@ async def handle_message(msg: str): event = NostrEvent(**event) if event.kind == 4: merchant = await get_merchant_by_pubkey(public_key) - if not merchant: - return + assert merchant, f"Merchant not found for public key '{public_key}'" + clear_text_msg = merchant.decrypt_message(event.content, event.pubkey) - print("### clear_text_msg", clear_text_msg) + await handle_nip04_message( + event.pubkey, event.id, clear_text_msg + ) except Exception as ex: logger.warning(ex) + + +async def handle_nip04_message(from_pubkey: str, event_id: str, msg: str): + order, text_msg = order_from_json(msg) + try: + if order: + print("### order", from_pubkey, event_id, msg) + ### check that event_id not parsed already + order["pubkey"] = from_pubkey + order["event_id"] = event_id + partial_order = PartialOrder(**order) + assert len(partial_order.items) != 0, "Order has no items. Order: " + msg + + first_product_id = partial_order.items[0].product_id + wallet_id = await get_wallet_for_product(first_product_id) + assert ( + wallet_id + ), f"Cannot find wallet id for product id: {first_product_id}" + + wallet = await get_wallet(wallet_id) + assert wallet, f"Cannot find wallet for product id: {first_product_id}" + + market_url = url_for(f"/nostrmarket/api/v1/order", external=True) + async with httpx.AsyncClient() as client: + await client.post( + url=market_url, + headers={ + "X-Api-Key": wallet.adminkey, + }, + json=order, + ) + else: + print("### text_msg", text_msg) + except Exception as ex: + logger.warning(ex) diff --git a/views_api.py b/views_api.py index ecb14f9..2102dc9 100644 --- a/views_api.py +++ b/views_api.py @@ -5,6 +5,7 @@ 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, @@ -17,6 +18,7 @@ from lnbits.utils.exchange_rates import currencies from . import nostrmarket_ext from .crud import ( create_merchant, + create_order, create_product, create_stall, create_zone, @@ -26,8 +28,10 @@ from .crud import ( get_merchant_for_user, get_product, get_products, + get_products_by_ids, get_stall, get_stalls, + get_wallet_for_product, get_zone, get_zones, update_product, @@ -37,7 +41,9 @@ from .crud import ( from .models import ( Merchant, Nostrable, + Order, PartialMerchant, + PartialOrder, PartialProduct, PartialStall, PartialZone, @@ -101,7 +107,7 @@ async def api_get_zones(wallet: WalletTypeInfo = Depends(get_key_type)) -> List[ @nostrmarket_ext.post("/api/v1/zone") async def api_create_zone( - data: PartialZone, wallet: WalletTypeInfo = Depends(get_key_type) + data: PartialZone, wallet: WalletTypeInfo = Depends(require_admin_key) ): try: zone = await create_zone(wallet.wallet.user, data) @@ -418,6 +424,50 @@ async def api_delete_product( ) +######################################## ORDERS ######################################## + + +@nostrmarket_ext.post("/api/v1/order") +async def api_create_order( + data: PartialOrder, wallet: WalletTypeInfo = Depends(require_admin_key) +): + try: + data.id = data.id or data.event_id + + wallet_id = await get_wallet_for_product(data.items[0].product_id) + assert wallet_id, "Missing wallet for order `{data.id}`" + + product_ids = [p.product_id for p in data.items] + products = await get_products_by_ids(wallet.wallet.user, product_ids) + + product_prices = {} + for p in products: + product_prices[p.id] = p + + amount: float = 0 # todo + for item in data.items: + amount += item.quantity * product_prices[item.product_id].price + + payment_hash, payment_request = await create_invoice( + wallet_id=wallet_id, + amount=round(amount), + memo=f"Order '{data.id}' for pubkey '{data.pubkey}'", + extra={ + "tag": "nostrmarket", + "order_id": data.id, + } + ) + + order = Order(**data.dict(), invoice_id=payment_hash, total=100) + await create_order(wallet.wallet.user, order) + except Exception as ex: + logger.warning(ex) + raise HTTPException( + status_code=HTTPStatus.INTERNAL_SERVER_ERROR, + detail="Cannot create order", + ) + + ######################################## OTHER ######################################## From bee52340a28dd3d8273de282cf77b647a7650718 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Mon, 6 Mar 2023 16:31:34 +0200 Subject: [PATCH 145/891] feat: return payment-request for order --- crud.py | 19 ++++++++++++++++--- helpers.py | 4 +++- models.py | 33 ++++++++++++++++++++++++++++++--- tasks.py | 10 ++++++---- views_api.py | 43 +++++++++++++++++++++++++------------------ 5 files changed, 80 insertions(+), 29 deletions(-) diff --git a/crud.py b/crud.py index d443dd0..6e362b4 100644 --- a/crud.py +++ b/crud.py @@ -283,13 +283,12 @@ async def get_products(user_id: str, stall_id: str) -> List[Product]: async def get_products_by_ids(user_id: str, product_ids: List[str]) -> List[Product]: q = ",".join(["?"] * len(product_ids)) rows = await db.fetchall( - f"SELECT id, stall_id, name, price, quantity FROM nostrmarket.products WHERE user_id = ? AND id IN ({q})", + f"SELECT id, stall_id, name, price, quantity, category_list, meta FROM nostrmarket.products WHERE user_id = ? AND id IN ({q})", (user_id, *product_ids), ) return [Product.from_row(row) for row in rows] - async def get_wallet_for_product(product_id: str) -> Optional[str]: row = await db.fetchone( """ @@ -312,8 +311,10 @@ async def delete_product(user_id: str, product_id: str) -> None: ), ) + ######################################## ORDERS ######################################## + async def create_order(user_id: str, o: Order) -> Order: await db.execute( f""" @@ -325,7 +326,7 @@ async def create_order(user_id: str, o: Order) -> Order: o.id, o.event_id, o.pubkey, - json.dumps(o.contact.dict()), + json.dumps(o.contact.dict() if o.contact else {}), json.dumps([i.dict() for i in o.items]), o.invoice_id, o.total, @@ -336,6 +337,7 @@ async def create_order(user_id: str, o: Order) -> Order: return order + async def get_order(user_id: str, order_id: str) -> Optional[Order]: row = await db.fetchone( "SELECT * FROM nostrmarket.orders WHERE user_id =? AND id = ?", @@ -345,3 +347,14 @@ async def get_order(user_id: str, order_id: str) -> Optional[Order]: ), ) return Order.from_row(row) if row else None + + +async def get_order_by_event_id(user_id: str, event_id: str) -> Optional[Order]: + row = await db.fetchone( + "SELECT * FROM nostrmarket.orders WHERE user_id =? AND event_id =?", + ( + user_id, + event_id, + ), + ) + return Order.from_row(row) if row else None diff --git a/helpers.py b/helpers.py index 5141009..706d2c6 100644 --- a/helpers.py +++ b/helpers.py @@ -76,6 +76,8 @@ def copy_x(output, x32, y32, data): def order_from_json(s: str) -> Tuple[Optional[Any], Optional[str]]: try: order = json.loads(s) - return (order, None) if "items" in order else (None, s) + return ( + (order, None) if (type(order) is dict) and "items" in order else (None, s) + ) except ValueError: return None, s diff --git a/models.py b/models.py index 2747085..945c1ae 100644 --- a/models.py +++ b/models.py @@ -6,6 +6,8 @@ from typing import List, Optional from pydantic import BaseModel +from lnbits.utils.exchange_rates import fiat_amount_as_satoshis + from .helpers import decrypt_message, get_shared_secret, sign_message_hash from .nostr.event import NostrEvent @@ -234,15 +236,29 @@ class OrderContact(BaseModel): class PartialOrder(BaseModel): - id: Optional[str] + id: str event_id: Optional[str] pubkey: str items: List[OrderItem] contact: Optional[OrderContact] + async def total_sats(self, products: List[Product]) -> float: + product_prices = {} + for p in products: + product_prices[p.id] = p + + amount: float = 0 # todo + for item in self.items: + price = product_prices[item.product_id].price + currency = product_prices[item.product_id].config.currency or "sat" + if currency != "sat": + price = await fiat_amount_as_satoshis(price, currency) + amount += item.quantity * price + + return amount + class Order(PartialOrder): - id: str invoice_id: str total: float paid: bool = False @@ -253,4 +269,15 @@ class Order(PartialOrder): contact = OrderContact(**json.loads(row["contact_data"])) items = [OrderItem(**z) for z in json.loads(row["order_items"])] order = cls(**dict(row), contact=contact, items=items) - return order \ No newline at end of file + return order + + +class PaymentOption(BaseModel): + type: str + link: str + + +class PaymentRequest(BaseModel): + id: str + message: Optional[str] + payment_options: List[PaymentOption] diff --git a/tasks.py b/tasks.py index 4ee1e4d..b8bc11f 100644 --- a/tasks.py +++ b/tasks.py @@ -94,9 +94,7 @@ async def handle_message(msg: str): assert merchant, f"Merchant not found for public key '{public_key}'" clear_text_msg = merchant.decrypt_message(event.content, event.pubkey) - await handle_nip04_message( - event.pubkey, event.id, clear_text_msg - ) + await handle_nip04_message(event.pubkey, event.id, clear_text_msg) except Exception as ex: logger.warning(ex) @@ -124,13 +122,17 @@ async def handle_nip04_message(from_pubkey: str, event_id: str, msg: str): market_url = url_for(f"/nostrmarket/api/v1/order", external=True) async with httpx.AsyncClient() as client: - await client.post( + resp = await client.post( url=market_url, headers={ "X-Api-Key": wallet.adminkey, }, json=order, ) + resp.raise_for_status() + data = resp.json() + + print("### payment request", data) else: print("### text_msg", text_msg) except Exception as ex: diff --git a/views_api.py b/views_api.py index 2102dc9..56d45fe 100644 --- a/views_api.py +++ b/views_api.py @@ -5,8 +5,8 @@ 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.core import create_invoice from lnbits.decorators import ( WalletTypeInfo, get_key_type, @@ -26,6 +26,8 @@ from .crud import ( delete_stall, delete_zone, get_merchant_for_user, + get_order, + get_order_by_event_id, get_product, get_products, get_products_by_ids, @@ -47,6 +49,8 @@ from .models import ( PartialProduct, PartialStall, PartialZone, + PaymentOption, + PaymentRequest, Product, Stall, Zone, @@ -430,36 +434,39 @@ async def api_delete_product( @nostrmarket_ext.post("/api/v1/order") async def api_create_order( data: PartialOrder, wallet: WalletTypeInfo = Depends(require_admin_key) -): +) -> Optional[PaymentRequest]: try: - data.id = data.id or data.event_id + 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 + + products = await get_products_by_ids( + wallet.wallet.user, [p.product_id for p in data.items] + ) + 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}`" - - product_ids = [p.product_id for p in data.items] - products = await get_products_by_ids(wallet.wallet.user, product_ids) - product_prices = {} - for p in products: - product_prices[p.id] = p - - amount: float = 0 # todo - for item in data.items: - amount += item.quantity * product_prices[item.product_id].price - - payment_hash, payment_request = await create_invoice( + payment_hash, invoice = await create_invoice( wallet_id=wallet_id, - amount=round(amount), + amount=round(total_amount), memo=f"Order '{data.id}' for pubkey '{data.pubkey}'", extra={ "tag": "nostrmarket", "order_id": data.id, - } + }, ) - order = Order(**data.dict(), invoice_id=payment_hash, total=100) + order = Order(**data.dict(), invoice_id=payment_hash, total=total_amount) 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( From 12c32b019ff014cadfe2d079d8d0f765c39c4a1c Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Mon, 6 Mar 2023 16:31:34 +0200 Subject: [PATCH 146/891] feat: return payment-request for order --- crud.py | 19 ++++++++++++++++--- helpers.py | 4 +++- models.py | 33 ++++++++++++++++++++++++++++++--- tasks.py | 10 ++++++---- views_api.py | 43 +++++++++++++++++++++++++------------------ 5 files changed, 80 insertions(+), 29 deletions(-) diff --git a/crud.py b/crud.py index d443dd0..6e362b4 100644 --- a/crud.py +++ b/crud.py @@ -283,13 +283,12 @@ async def get_products(user_id: str, stall_id: str) -> List[Product]: async def get_products_by_ids(user_id: str, product_ids: List[str]) -> List[Product]: q = ",".join(["?"] * len(product_ids)) rows = await db.fetchall( - f"SELECT id, stall_id, name, price, quantity FROM nostrmarket.products WHERE user_id = ? AND id IN ({q})", + f"SELECT id, stall_id, name, price, quantity, category_list, meta FROM nostrmarket.products WHERE user_id = ? AND id IN ({q})", (user_id, *product_ids), ) return [Product.from_row(row) for row in rows] - async def get_wallet_for_product(product_id: str) -> Optional[str]: row = await db.fetchone( """ @@ -312,8 +311,10 @@ async def delete_product(user_id: str, product_id: str) -> None: ), ) + ######################################## ORDERS ######################################## + async def create_order(user_id: str, o: Order) -> Order: await db.execute( f""" @@ -325,7 +326,7 @@ async def create_order(user_id: str, o: Order) -> Order: o.id, o.event_id, o.pubkey, - json.dumps(o.contact.dict()), + json.dumps(o.contact.dict() if o.contact else {}), json.dumps([i.dict() for i in o.items]), o.invoice_id, o.total, @@ -336,6 +337,7 @@ async def create_order(user_id: str, o: Order) -> Order: return order + async def get_order(user_id: str, order_id: str) -> Optional[Order]: row = await db.fetchone( "SELECT * FROM nostrmarket.orders WHERE user_id =? AND id = ?", @@ -345,3 +347,14 @@ async def get_order(user_id: str, order_id: str) -> Optional[Order]: ), ) return Order.from_row(row) if row else None + + +async def get_order_by_event_id(user_id: str, event_id: str) -> Optional[Order]: + row = await db.fetchone( + "SELECT * FROM nostrmarket.orders WHERE user_id =? AND event_id =?", + ( + user_id, + event_id, + ), + ) + return Order.from_row(row) if row else None diff --git a/helpers.py b/helpers.py index 5141009..706d2c6 100644 --- a/helpers.py +++ b/helpers.py @@ -76,6 +76,8 @@ def copy_x(output, x32, y32, data): def order_from_json(s: str) -> Tuple[Optional[Any], Optional[str]]: try: order = json.loads(s) - return (order, None) if "items" in order else (None, s) + return ( + (order, None) if (type(order) is dict) and "items" in order else (None, s) + ) except ValueError: return None, s diff --git a/models.py b/models.py index 2747085..945c1ae 100644 --- a/models.py +++ b/models.py @@ -6,6 +6,8 @@ from typing import List, Optional from pydantic import BaseModel +from lnbits.utils.exchange_rates import fiat_amount_as_satoshis + from .helpers import decrypt_message, get_shared_secret, sign_message_hash from .nostr.event import NostrEvent @@ -234,15 +236,29 @@ class OrderContact(BaseModel): class PartialOrder(BaseModel): - id: Optional[str] + id: str event_id: Optional[str] pubkey: str items: List[OrderItem] contact: Optional[OrderContact] + async def total_sats(self, products: List[Product]) -> float: + product_prices = {} + for p in products: + product_prices[p.id] = p + + amount: float = 0 # todo + for item in self.items: + price = product_prices[item.product_id].price + currency = product_prices[item.product_id].config.currency or "sat" + if currency != "sat": + price = await fiat_amount_as_satoshis(price, currency) + amount += item.quantity * price + + return amount + class Order(PartialOrder): - id: str invoice_id: str total: float paid: bool = False @@ -253,4 +269,15 @@ class Order(PartialOrder): contact = OrderContact(**json.loads(row["contact_data"])) items = [OrderItem(**z) for z in json.loads(row["order_items"])] order = cls(**dict(row), contact=contact, items=items) - return order \ No newline at end of file + return order + + +class PaymentOption(BaseModel): + type: str + link: str + + +class PaymentRequest(BaseModel): + id: str + message: Optional[str] + payment_options: List[PaymentOption] diff --git a/tasks.py b/tasks.py index 4ee1e4d..b8bc11f 100644 --- a/tasks.py +++ b/tasks.py @@ -94,9 +94,7 @@ async def handle_message(msg: str): assert merchant, f"Merchant not found for public key '{public_key}'" clear_text_msg = merchant.decrypt_message(event.content, event.pubkey) - await handle_nip04_message( - event.pubkey, event.id, clear_text_msg - ) + await handle_nip04_message(event.pubkey, event.id, clear_text_msg) except Exception as ex: logger.warning(ex) @@ -124,13 +122,17 @@ async def handle_nip04_message(from_pubkey: str, event_id: str, msg: str): market_url = url_for(f"/nostrmarket/api/v1/order", external=True) async with httpx.AsyncClient() as client: - await client.post( + resp = await client.post( url=market_url, headers={ "X-Api-Key": wallet.adminkey, }, json=order, ) + resp.raise_for_status() + data = resp.json() + + print("### payment request", data) else: print("### text_msg", text_msg) except Exception as ex: diff --git a/views_api.py b/views_api.py index 2102dc9..56d45fe 100644 --- a/views_api.py +++ b/views_api.py @@ -5,8 +5,8 @@ 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.core import create_invoice from lnbits.decorators import ( WalletTypeInfo, get_key_type, @@ -26,6 +26,8 @@ from .crud import ( delete_stall, delete_zone, get_merchant_for_user, + get_order, + get_order_by_event_id, get_product, get_products, get_products_by_ids, @@ -47,6 +49,8 @@ from .models import ( PartialProduct, PartialStall, PartialZone, + PaymentOption, + PaymentRequest, Product, Stall, Zone, @@ -430,36 +434,39 @@ async def api_delete_product( @nostrmarket_ext.post("/api/v1/order") async def api_create_order( data: PartialOrder, wallet: WalletTypeInfo = Depends(require_admin_key) -): +) -> Optional[PaymentRequest]: try: - data.id = data.id or data.event_id + 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 + + products = await get_products_by_ids( + wallet.wallet.user, [p.product_id for p in data.items] + ) + 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}`" - - product_ids = [p.product_id for p in data.items] - products = await get_products_by_ids(wallet.wallet.user, product_ids) - product_prices = {} - for p in products: - product_prices[p.id] = p - - amount: float = 0 # todo - for item in data.items: - amount += item.quantity * product_prices[item.product_id].price - - payment_hash, payment_request = await create_invoice( + payment_hash, invoice = await create_invoice( wallet_id=wallet_id, - amount=round(amount), + amount=round(total_amount), memo=f"Order '{data.id}' for pubkey '{data.pubkey}'", extra={ "tag": "nostrmarket", "order_id": data.id, - } + }, ) - order = Order(**data.dict(), invoice_id=payment_hash, total=100) + order = Order(**data.dict(), invoice_id=payment_hash, total=total_amount) 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( From 35298a4f44eff66fe732f7b465a9007656dee522 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Mon, 6 Mar 2023 17:33:55 +0200 Subject: [PATCH 147/891] feat: send invoice back for order --- helpers.py | 2 +- models.py | 40 +++++++++++++++++++++++++++++++++++++++- tasks.py | 29 ++++++++++++++++++++--------- views_api.py | 1 - 4 files changed, 60 insertions(+), 12 deletions(-) diff --git a/helpers.py b/helpers.py index 706d2c6..ee73cd9 100644 --- a/helpers.py +++ b/helpers.py @@ -31,7 +31,7 @@ def decrypt_message(encoded_message: str, encryption_key) -> str: return unpadded_data.decode() -def encrypt_message(message: str, encryption_key, iv: Optional[bytes]) -> str: +def encrypt_message(message: str, encryption_key, iv: Optional[bytes] = None) -> str: padder = padding.PKCS7(128).padder() padded_data = padder.update(message.encode()) + padder.finalize() diff --git a/models.py b/models.py index 945c1ae..cd93d38 100644 --- a/models.py +++ b/models.py @@ -8,7 +8,12 @@ from pydantic import BaseModel from lnbits.utils.exchange_rates import fiat_amount_as_satoshis -from .helpers import decrypt_message, get_shared_secret, sign_message_hash +from .helpers import ( + decrypt_message, + encrypt_message, + get_shared_secret, + sign_message_hash, +) from .nostr.event import NostrEvent ######################################## NOSTR ######################################## @@ -45,6 +50,24 @@ class Merchant(PartialMerchant): encryption_key = get_shared_secret(self.private_key, public_key) return decrypt_message(encrypted_message, encryption_key) + def encrypt_message(self, clear_text_message: str, public_key: str) -> str: + encryption_key = get_shared_secret(self.private_key, public_key) + return encrypt_message(clear_text_message, encryption_key) + + def build_dm_event(self, message: str, to_pubkey: str) -> NostrEvent: + content = self.encrypt_message(message, to_pubkey) + event = NostrEvent( + pubkey=self.public_key, + created_at=round(time.time()), + kind=4, + tags=[["p", to_pubkey]], + content=content, + ) + event.id = event.event_id + event.sig = self.sign_hash(bytes.fromhex(event.id)) + + return event + @classmethod def from_row(cls, row: Row) -> "Merchant": merchant = cls(**dict(row)) @@ -242,6 +265,9 @@ class PartialOrder(BaseModel): items: List[OrderItem] contact: Optional[OrderContact] + def validate_order(self): + assert len(self.items) != 0, f"Order has no items. Order: '{self.id}'" + async def total_sats(self, products: List[Product]) -> float: product_prices = {} for p in products: @@ -281,3 +307,15 @@ class PaymentRequest(BaseModel): id: str message: Optional[str] payment_options: List[PaymentOption] + + def to_nostr_event(self, author_pubkey: str, to_pubkey: str) -> NostrEvent: + event = NostrEvent( + pubkey=author_pubkey, + created_at=round(time.time()), + kind=4, + tags=[["p", to_pubkey]], + content=json.dumps(self.dict(), separators=(",", ":"), ensure_ascii=False), + ) + event.id = event.event_id + + return event diff --git a/tasks.py b/tasks.py index b8bc11f..b5e9619 100644 --- a/tasks.py +++ b/tasks.py @@ -9,19 +9,18 @@ from websocket import WebSocketApp from lnbits.core import get_wallet from lnbits.core.models import Payment -from lnbits.extensions.nostrmarket.models import PartialOrder -from lnbits.helpers import url_for +from lnbits.helpers import Optional, url_for from lnbits.tasks import register_invoice_listener from .crud import ( get_merchant_by_pubkey, - get_product, get_public_keys_for_merchants, get_wallet_for_product, ) from .helpers import order_from_json +from .models import PartialOrder from .nostr.event import NostrEvent -from .nostr.nostr_client import connect_to_nostrclient_ws +from .nostr.nostr_client import connect_to_nostrclient_ws, publish_nostr_event async def wait_for_paid_invoices(): @@ -94,21 +93,28 @@ async def handle_message(msg: str): assert merchant, f"Merchant not found for public key '{public_key}'" clear_text_msg = merchant.decrypt_message(event.content, event.pubkey) - await handle_nip04_message(event.pubkey, event.id, clear_text_msg) + dm_resp = await handle_dirrect_message( + event.pubkey, event.id, clear_text_msg + ) + if dm_resp: + dm_event = merchant.build_dm_event(dm_resp, event.pubkey) + await publish_nostr_event(dm_event) except Exception as ex: logger.warning(ex) -async def handle_nip04_message(from_pubkey: str, event_id: str, msg: str): +async def handle_dirrect_message( + from_pubkey: str, event_id: str, msg: str +) -> Optional[str]: order, text_msg = order_from_json(msg) try: if order: - print("### order", from_pubkey, event_id, msg) ### check that event_id not parsed already order["pubkey"] = from_pubkey order["event_id"] = event_id partial_order = PartialOrder(**order) + partial_order.validate_order() assert len(partial_order.items) != 0, "Order has no items. Order: " + msg first_product_id = partial_order.items[0].product_id @@ -131,9 +137,14 @@ async def handle_nip04_message(from_pubkey: str, event_id: str, msg: str): ) resp.raise_for_status() data = resp.json() - - print("### payment request", data) + return ( + json.dumps(data, separators=(",", ":"), ensure_ascii=False) + if data + else None + ) else: print("### text_msg", text_msg) + return None except Exception as ex: logger.warning(ex) + return None diff --git a/views_api.py b/views_api.py index 56d45fe..bca744d 100644 --- a/views_api.py +++ b/views_api.py @@ -1,4 +1,3 @@ -import json from http import HTTPStatus from typing import List, Optional From 8cdcb144b9ee9efa2696c9a403634cd134ecd5e7 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Mon, 6 Mar 2023 17:33:55 +0200 Subject: [PATCH 148/891] feat: send invoice back for order --- helpers.py | 2 +- models.py | 40 +++++++++++++++++++++++++++++++++++++++- tasks.py | 29 ++++++++++++++++++++--------- views_api.py | 1 - 4 files changed, 60 insertions(+), 12 deletions(-) diff --git a/helpers.py b/helpers.py index 706d2c6..ee73cd9 100644 --- a/helpers.py +++ b/helpers.py @@ -31,7 +31,7 @@ def decrypt_message(encoded_message: str, encryption_key) -> str: return unpadded_data.decode() -def encrypt_message(message: str, encryption_key, iv: Optional[bytes]) -> str: +def encrypt_message(message: str, encryption_key, iv: Optional[bytes] = None) -> str: padder = padding.PKCS7(128).padder() padded_data = padder.update(message.encode()) + padder.finalize() diff --git a/models.py b/models.py index 945c1ae..cd93d38 100644 --- a/models.py +++ b/models.py @@ -8,7 +8,12 @@ from pydantic import BaseModel from lnbits.utils.exchange_rates import fiat_amount_as_satoshis -from .helpers import decrypt_message, get_shared_secret, sign_message_hash +from .helpers import ( + decrypt_message, + encrypt_message, + get_shared_secret, + sign_message_hash, +) from .nostr.event import NostrEvent ######################################## NOSTR ######################################## @@ -45,6 +50,24 @@ class Merchant(PartialMerchant): encryption_key = get_shared_secret(self.private_key, public_key) return decrypt_message(encrypted_message, encryption_key) + def encrypt_message(self, clear_text_message: str, public_key: str) -> str: + encryption_key = get_shared_secret(self.private_key, public_key) + return encrypt_message(clear_text_message, encryption_key) + + def build_dm_event(self, message: str, to_pubkey: str) -> NostrEvent: + content = self.encrypt_message(message, to_pubkey) + event = NostrEvent( + pubkey=self.public_key, + created_at=round(time.time()), + kind=4, + tags=[["p", to_pubkey]], + content=content, + ) + event.id = event.event_id + event.sig = self.sign_hash(bytes.fromhex(event.id)) + + return event + @classmethod def from_row(cls, row: Row) -> "Merchant": merchant = cls(**dict(row)) @@ -242,6 +265,9 @@ class PartialOrder(BaseModel): items: List[OrderItem] contact: Optional[OrderContact] + def validate_order(self): + assert len(self.items) != 0, f"Order has no items. Order: '{self.id}'" + async def total_sats(self, products: List[Product]) -> float: product_prices = {} for p in products: @@ -281,3 +307,15 @@ class PaymentRequest(BaseModel): id: str message: Optional[str] payment_options: List[PaymentOption] + + def to_nostr_event(self, author_pubkey: str, to_pubkey: str) -> NostrEvent: + event = NostrEvent( + pubkey=author_pubkey, + created_at=round(time.time()), + kind=4, + tags=[["p", to_pubkey]], + content=json.dumps(self.dict(), separators=(",", ":"), ensure_ascii=False), + ) + event.id = event.event_id + + return event diff --git a/tasks.py b/tasks.py index b8bc11f..b5e9619 100644 --- a/tasks.py +++ b/tasks.py @@ -9,19 +9,18 @@ from websocket import WebSocketApp from lnbits.core import get_wallet from lnbits.core.models import Payment -from lnbits.extensions.nostrmarket.models import PartialOrder -from lnbits.helpers import url_for +from lnbits.helpers import Optional, url_for from lnbits.tasks import register_invoice_listener from .crud import ( get_merchant_by_pubkey, - get_product, get_public_keys_for_merchants, get_wallet_for_product, ) from .helpers import order_from_json +from .models import PartialOrder from .nostr.event import NostrEvent -from .nostr.nostr_client import connect_to_nostrclient_ws +from .nostr.nostr_client import connect_to_nostrclient_ws, publish_nostr_event async def wait_for_paid_invoices(): @@ -94,21 +93,28 @@ async def handle_message(msg: str): assert merchant, f"Merchant not found for public key '{public_key}'" clear_text_msg = merchant.decrypt_message(event.content, event.pubkey) - await handle_nip04_message(event.pubkey, event.id, clear_text_msg) + dm_resp = await handle_dirrect_message( + event.pubkey, event.id, clear_text_msg + ) + if dm_resp: + dm_event = merchant.build_dm_event(dm_resp, event.pubkey) + await publish_nostr_event(dm_event) except Exception as ex: logger.warning(ex) -async def handle_nip04_message(from_pubkey: str, event_id: str, msg: str): +async def handle_dirrect_message( + from_pubkey: str, event_id: str, msg: str +) -> Optional[str]: order, text_msg = order_from_json(msg) try: if order: - print("### order", from_pubkey, event_id, msg) ### check that event_id not parsed already order["pubkey"] = from_pubkey order["event_id"] = event_id partial_order = PartialOrder(**order) + partial_order.validate_order() assert len(partial_order.items) != 0, "Order has no items. Order: " + msg first_product_id = partial_order.items[0].product_id @@ -131,9 +137,14 @@ async def handle_nip04_message(from_pubkey: str, event_id: str, msg: str): ) resp.raise_for_status() data = resp.json() - - print("### payment request", data) + return ( + json.dumps(data, separators=(",", ":"), ensure_ascii=False) + if data + else None + ) else: print("### text_msg", text_msg) + return None except Exception as ex: logger.warning(ex) + return None diff --git a/views_api.py b/views_api.py index 56d45fe..bca744d 100644 --- a/views_api.py +++ b/views_api.py @@ -1,4 +1,3 @@ -import json from http import HTTPStatus from typing import List, Optional From 1b317b1b9b763bd406ef9fb6607d78755bdf3203 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Mon, 6 Mar 2023 18:01:05 +0200 Subject: [PATCH 149/891] refactor: extract simpler methods --- models.py | 13 +-------- tasks.py | 85 +++++++++++++++++++++++++++++-------------------------- 2 files changed, 46 insertions(+), 52 deletions(-) diff --git a/models.py b/models.py index cd93d38..711a3f4 100644 --- a/models.py +++ b/models.py @@ -256,6 +256,7 @@ class OrderContact(BaseModel): nostr: Optional[str] phone: Optional[str] email: Optional[str] + address: Optional[str] class PartialOrder(BaseModel): @@ -307,15 +308,3 @@ class PaymentRequest(BaseModel): id: str message: Optional[str] payment_options: List[PaymentOption] - - def to_nostr_event(self, author_pubkey: str, to_pubkey: str) -> NostrEvent: - event = NostrEvent( - pubkey=author_pubkey, - created_at=round(time.time()), - kind=4, - tags=[["p", to_pubkey]], - content=json.dumps(self.dict(), separators=(",", ":"), ensure_ascii=False), - ) - event.id = event.event_id - - return event diff --git a/tasks.py b/tasks.py index b5e9619..9a78344 100644 --- a/tasks.py +++ b/tasks.py @@ -89,62 +89,67 @@ async def handle_message(msg: str): if type.upper() == "EVENT": event = NostrEvent(**event) if event.kind == 4: - merchant = await get_merchant_by_pubkey(public_key) - assert merchant, f"Merchant not found for public key '{public_key}'" - - clear_text_msg = merchant.decrypt_message(event.content, event.pubkey) - dm_resp = await handle_dirrect_message( - event.pubkey, event.id, clear_text_msg - ) - if dm_resp: - dm_event = merchant.build_dm_event(dm_resp, event.pubkey) - await publish_nostr_event(dm_event) + await handle_nip04_message(public_key, event) except Exception as ex: logger.warning(ex) +async def handle_nip04_message(public_key: str, event: NostrEvent): + merchant = await get_merchant_by_pubkey(public_key) + assert merchant, f"Merchant not found for public key '{public_key}'" + + clear_text_msg = merchant.decrypt_message(event.content, event.pubkey) + dm_resp = await handle_dirrect_message(event.pubkey, event.id, clear_text_msg) + if dm_resp: + dm_event = merchant.build_dm_event(dm_resp, event.pubkey) + await publish_nostr_event(dm_event) + + async def handle_dirrect_message( from_pubkey: str, event_id: str, msg: str ) -> Optional[str]: order, text_msg = order_from_json(msg) try: if order: - ### check that event_id not parsed already order["pubkey"] = from_pubkey order["event_id"] = event_id - partial_order = PartialOrder(**order) - partial_order.validate_order() - assert len(partial_order.items) != 0, "Order has no items. Order: " + msg - - first_product_id = partial_order.items[0].product_id - wallet_id = await get_wallet_for_product(first_product_id) - assert ( - wallet_id - ), f"Cannot find wallet id for product id: {first_product_id}" - - wallet = await get_wallet(wallet_id) - assert wallet, f"Cannot find wallet for product id: {first_product_id}" - - market_url = url_for(f"/nostrmarket/api/v1/order", external=True) - async with httpx.AsyncClient() as client: - resp = await client.post( - url=market_url, - headers={ - "X-Api-Key": wallet.adminkey, - }, - json=order, - ) - resp.raise_for_status() - data = resp.json() - return ( - json.dumps(data, separators=(",", ":"), ensure_ascii=False) - if data - else None - ) + return await handle_new_order(PartialOrder(**order)) else: print("### text_msg", text_msg) return None except Exception as ex: logger.warning(ex) return None + + +async def handle_new_order(order: PartialOrder): + ### check that event_id not parsed already + + order.validate_order() + assert ( + len(order.items) != 0 + ), f"Order has no items. Order: '{order.id}' ({order.event_id})" + + first_product_id = order.items[0].product_id + wallet_id = await get_wallet_for_product(first_product_id) + assert wallet_id, f"Cannot find wallet id for product id: {first_product_id}" + + wallet = await get_wallet(wallet_id) + assert wallet, f"Cannot find wallet for product id: {first_product_id}" + + market_url = url_for(f"/nostrmarket/api/v1/order", external=True) + async with httpx.AsyncClient() as client: + resp = await client.post( + url=market_url, + headers={ + "X-Api-Key": wallet.adminkey, + }, + json=order.dict(), + ) + resp.raise_for_status() + data = resp.json() + if data: + return json.dumps(data, separators=(",", ":"), ensure_ascii=False) + + return None From dd47c6c64ba901168e0be471a87a875b567a6801 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Mon, 6 Mar 2023 18:01:05 +0200 Subject: [PATCH 150/891] refactor: extract simpler methods --- models.py | 13 +-------- tasks.py | 85 +++++++++++++++++++++++++++++-------------------------- 2 files changed, 46 insertions(+), 52 deletions(-) diff --git a/models.py b/models.py index cd93d38..711a3f4 100644 --- a/models.py +++ b/models.py @@ -256,6 +256,7 @@ class OrderContact(BaseModel): nostr: Optional[str] phone: Optional[str] email: Optional[str] + address: Optional[str] class PartialOrder(BaseModel): @@ -307,15 +308,3 @@ class PaymentRequest(BaseModel): id: str message: Optional[str] payment_options: List[PaymentOption] - - def to_nostr_event(self, author_pubkey: str, to_pubkey: str) -> NostrEvent: - event = NostrEvent( - pubkey=author_pubkey, - created_at=round(time.time()), - kind=4, - tags=[["p", to_pubkey]], - content=json.dumps(self.dict(), separators=(",", ":"), ensure_ascii=False), - ) - event.id = event.event_id - - return event diff --git a/tasks.py b/tasks.py index b5e9619..9a78344 100644 --- a/tasks.py +++ b/tasks.py @@ -89,62 +89,67 @@ async def handle_message(msg: str): if type.upper() == "EVENT": event = NostrEvent(**event) if event.kind == 4: - merchant = await get_merchant_by_pubkey(public_key) - assert merchant, f"Merchant not found for public key '{public_key}'" - - clear_text_msg = merchant.decrypt_message(event.content, event.pubkey) - dm_resp = await handle_dirrect_message( - event.pubkey, event.id, clear_text_msg - ) - if dm_resp: - dm_event = merchant.build_dm_event(dm_resp, event.pubkey) - await publish_nostr_event(dm_event) + await handle_nip04_message(public_key, event) except Exception as ex: logger.warning(ex) +async def handle_nip04_message(public_key: str, event: NostrEvent): + merchant = await get_merchant_by_pubkey(public_key) + assert merchant, f"Merchant not found for public key '{public_key}'" + + clear_text_msg = merchant.decrypt_message(event.content, event.pubkey) + dm_resp = await handle_dirrect_message(event.pubkey, event.id, clear_text_msg) + if dm_resp: + dm_event = merchant.build_dm_event(dm_resp, event.pubkey) + await publish_nostr_event(dm_event) + + async def handle_dirrect_message( from_pubkey: str, event_id: str, msg: str ) -> Optional[str]: order, text_msg = order_from_json(msg) try: if order: - ### check that event_id not parsed already order["pubkey"] = from_pubkey order["event_id"] = event_id - partial_order = PartialOrder(**order) - partial_order.validate_order() - assert len(partial_order.items) != 0, "Order has no items. Order: " + msg - - first_product_id = partial_order.items[0].product_id - wallet_id = await get_wallet_for_product(first_product_id) - assert ( - wallet_id - ), f"Cannot find wallet id for product id: {first_product_id}" - - wallet = await get_wallet(wallet_id) - assert wallet, f"Cannot find wallet for product id: {first_product_id}" - - market_url = url_for(f"/nostrmarket/api/v1/order", external=True) - async with httpx.AsyncClient() as client: - resp = await client.post( - url=market_url, - headers={ - "X-Api-Key": wallet.adminkey, - }, - json=order, - ) - resp.raise_for_status() - data = resp.json() - return ( - json.dumps(data, separators=(",", ":"), ensure_ascii=False) - if data - else None - ) + return await handle_new_order(PartialOrder(**order)) else: print("### text_msg", text_msg) return None except Exception as ex: logger.warning(ex) return None + + +async def handle_new_order(order: PartialOrder): + ### check that event_id not parsed already + + order.validate_order() + assert ( + len(order.items) != 0 + ), f"Order has no items. Order: '{order.id}' ({order.event_id})" + + first_product_id = order.items[0].product_id + wallet_id = await get_wallet_for_product(first_product_id) + assert wallet_id, f"Cannot find wallet id for product id: {first_product_id}" + + wallet = await get_wallet(wallet_id) + assert wallet, f"Cannot find wallet for product id: {first_product_id}" + + market_url = url_for(f"/nostrmarket/api/v1/order", external=True) + async with httpx.AsyncClient() as client: + resp = await client.post( + url=market_url, + headers={ + "X-Api-Key": wallet.adminkey, + }, + json=order.dict(), + ) + resp.raise_for_status() + data = resp.json() + if data: + return json.dumps(data, separators=(",", ":"), ensure_ascii=False) + + return None From da41ed365114656b4b4b5be14ab9445a216a98c1 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Mon, 6 Mar 2023 18:07:52 +0200 Subject: [PATCH 151/891] feat: add address for order --- crud.py | 5 +++-- migrations.py | 3 ++- models.py | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/crud.py b/crud.py index 6e362b4..c74512f 100644 --- a/crud.py +++ b/crud.py @@ -318,14 +318,15 @@ async def delete_product(user_id: str, product_id: str) -> None: async def create_order(user_id: str, o: Order) -> Order: await db.execute( f""" - INSERT INTO nostrmarket.orders (user_id, id, event_id, pubkey, contact_data, order_items, invoice_id, total) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) + INSERT INTO nostrmarket.orders (user_id, id, event_id, pubkey, address, contact_data, order_items, invoice_id, total) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( user_id, o.id, o.event_id, o.pubkey, + o.address, json.dumps(o.contact.dict() if o.contact else {}), json.dumps([i.dict() for i in o.items]), o.invoice_id, diff --git a/migrations.py b/migrations.py index eead2a7..4ed088b 100644 --- a/migrations.py +++ b/migrations.py @@ -78,9 +78,10 @@ async def m001_initial(db): user_id TEXT NOT NULL, id TEXT PRIMARY KEY, event_id TEXT, - pubkey TEXT, + pubkey EXT NOT NULL, contact_data TEXT NOT NULL DEFAULT '{empty_object}', order_items TEXT NOT NULL, + address TEXT, total REAL NOT NULL, invoice_id TEXT NOT NULL, paid BOOLEAN NOT NULL DEFAULT false, diff --git a/models.py b/models.py index 711a3f4..0cfee8f 100644 --- a/models.py +++ b/models.py @@ -256,7 +256,6 @@ class OrderContact(BaseModel): nostr: Optional[str] phone: Optional[str] email: Optional[str] - address: Optional[str] class PartialOrder(BaseModel): @@ -265,6 +264,7 @@ class PartialOrder(BaseModel): pubkey: str items: List[OrderItem] contact: Optional[OrderContact] + address: Optional[str] def validate_order(self): assert len(self.items) != 0, f"Order has no items. Order: '{self.id}'" From b7071d6796193f0858b9419f8dd4628e791c188d Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Mon, 6 Mar 2023 18:07:52 +0200 Subject: [PATCH 152/891] feat: add address for order --- crud.py | 5 +++-- migrations.py | 3 ++- models.py | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/crud.py b/crud.py index 6e362b4..c74512f 100644 --- a/crud.py +++ b/crud.py @@ -318,14 +318,15 @@ async def delete_product(user_id: str, product_id: str) -> None: async def create_order(user_id: str, o: Order) -> Order: await db.execute( f""" - INSERT INTO nostrmarket.orders (user_id, id, event_id, pubkey, contact_data, order_items, invoice_id, total) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) + INSERT INTO nostrmarket.orders (user_id, id, event_id, pubkey, address, contact_data, order_items, invoice_id, total) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( user_id, o.id, o.event_id, o.pubkey, + o.address, json.dumps(o.contact.dict() if o.contact else {}), json.dumps([i.dict() for i in o.items]), o.invoice_id, diff --git a/migrations.py b/migrations.py index eead2a7..4ed088b 100644 --- a/migrations.py +++ b/migrations.py @@ -78,9 +78,10 @@ async def m001_initial(db): user_id TEXT NOT NULL, id TEXT PRIMARY KEY, event_id TEXT, - pubkey TEXT, + pubkey EXT NOT NULL, contact_data TEXT NOT NULL DEFAULT '{empty_object}', order_items TEXT NOT NULL, + address TEXT, total REAL NOT NULL, invoice_id TEXT NOT NULL, paid BOOLEAN NOT NULL DEFAULT false, diff --git a/models.py b/models.py index 711a3f4..0cfee8f 100644 --- a/models.py +++ b/models.py @@ -256,7 +256,6 @@ class OrderContact(BaseModel): nostr: Optional[str] phone: Optional[str] email: Optional[str] - address: Optional[str] class PartialOrder(BaseModel): @@ -265,6 +264,7 @@ class PartialOrder(BaseModel): pubkey: str items: List[OrderItem] contact: Optional[OrderContact] + address: Optional[str] def validate_order(self): assert len(self.items) != 0, f"Order has no items. Order: '{self.id}'" From b446629707130ffa00a3f5bbd49949ecac3da0a2 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Tue, 7 Mar 2023 09:12:14 +0200 Subject: [PATCH 153/891] feat: add basic `order-list` --- static/components/order-list/order-list.html | 3 +++ static/components/order-list/order-list.js | 18 ++++++++++++++++++ .../stall-details/stall-details.html | 8 +++++++- static/js/index.js | 1 + templates/nostrmarket/index.html | 1 + 5 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 static/components/order-list/order-list.html create mode 100644 static/components/order-list/order-list.js diff --git a/static/components/order-list/order-list.html b/static/components/order-list/order-list.html new file mode 100644 index 0000000..147c21f --- /dev/null +++ b/static/components/order-list/order-list.html @@ -0,0 +1,3 @@ +
+ xx1 +
\ No newline at end of file diff --git a/static/components/order-list/order-list.js b/static/components/order-list/order-list.js new file mode 100644 index 0000000..15b1528 --- /dev/null +++ b/static/components/order-list/order-list.js @@ -0,0 +1,18 @@ +async function orderList(path) { + const template = await loadTemplateAsync(path) + Vue.component('order-list', { + name: 'order-list', + props: ['adminkey', 'inkey'], + template, + + data: function () { + return { + } + }, + methods: { + }, + created: async function () { + } + }) + } + \ No newline at end of file diff --git a/static/components/stall-details/stall-details.html b/static/components/stall-details/stall-details.html index b26caf3..9ffe49a 100644 --- a/static/components/stall-details/stall-details.html +++ b/static/components/stall-details/stall-details.html @@ -185,7 +185,13 @@
-
+
+ +
diff --git a/static/js/index.js b/static/js/index.js index 5b26d4c..3eb50ad 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -5,6 +5,7 @@ const merchant = async () => { await shippingZones('static/components/shipping-zones/shipping-zones.html') await stallDetails('static/components/stall-details/stall-details.html') await stallList('static/components/stall-list/stall-list.html') + await orderList('static/components/order-list/order-list.html') const nostr = window.NostrTools diff --git a/templates/nostrmarket/index.html b/templates/nostrmarket/index.html index fa07f17..b866226 100644 --- a/templates/nostrmarket/index.html +++ b/templates/nostrmarket/index.html @@ -150,6 +150,7 @@ + {% endblock %} From 0ca5bf6475ed7ded2dedac09847d623e31e01668 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Tue, 7 Mar 2023 09:12:14 +0200 Subject: [PATCH 154/891] feat: add basic `order-list` --- static/components/order-list/order-list.html | 3 +++ static/components/order-list/order-list.js | 18 ++++++++++++++++++ .../stall-details/stall-details.html | 8 +++++++- static/js/index.js | 1 + templates/nostrmarket/index.html | 1 + 5 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 static/components/order-list/order-list.html create mode 100644 static/components/order-list/order-list.js diff --git a/static/components/order-list/order-list.html b/static/components/order-list/order-list.html new file mode 100644 index 0000000..147c21f --- /dev/null +++ b/static/components/order-list/order-list.html @@ -0,0 +1,3 @@ +
+ xx1 +
\ No newline at end of file diff --git a/static/components/order-list/order-list.js b/static/components/order-list/order-list.js new file mode 100644 index 0000000..15b1528 --- /dev/null +++ b/static/components/order-list/order-list.js @@ -0,0 +1,18 @@ +async function orderList(path) { + const template = await loadTemplateAsync(path) + Vue.component('order-list', { + name: 'order-list', + props: ['adminkey', 'inkey'], + template, + + data: function () { + return { + } + }, + methods: { + }, + created: async function () { + } + }) + } + \ No newline at end of file diff --git a/static/components/stall-details/stall-details.html b/static/components/stall-details/stall-details.html index b26caf3..9ffe49a 100644 --- a/static/components/stall-details/stall-details.html +++ b/static/components/stall-details/stall-details.html @@ -185,7 +185,13 @@
-
+
+ +
diff --git a/static/js/index.js b/static/js/index.js index 5b26d4c..3eb50ad 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -5,6 +5,7 @@ const merchant = async () => { await shippingZones('static/components/shipping-zones/shipping-zones.html') await stallDetails('static/components/stall-details/stall-details.html') await stallList('static/components/stall-list/stall-list.html') + await orderList('static/components/order-list/order-list.html') const nostr = window.NostrTools diff --git a/templates/nostrmarket/index.html b/templates/nostrmarket/index.html index fa07f17..b866226 100644 --- a/templates/nostrmarket/index.html +++ b/templates/nostrmarket/index.html @@ -150,6 +150,7 @@ + {% endblock %} From 40c25ad085a63a406b0649bd0ffd70fdb8fbea3a Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Tue, 7 Mar 2023 10:31:25 +0200 Subject: [PATCH 155/891] feat: show orders per stall --- crud.py | 24 +++- migrations.py | 3 +- models.py | 22 ++++ static/components/order-list/order-list.html | 45 +++++++- static/components/order-list/order-list.js | 107 +++++++++++++++--- .../stall-details/stall-details.html | 8 +- static/components/stall-list/stall-list.html | 2 +- static/components/stall-list/stall-list.js | 6 + tasks.py | 3 - views_api.py | 64 ++++++++++- 10 files changed, 255 insertions(+), 29 deletions(-) diff --git a/crud.py b/crud.py index c74512f..be080b7 100644 --- a/crud.py +++ b/crud.py @@ -318,8 +318,8 @@ async def delete_product(user_id: str, product_id: str) -> None: async def create_order(user_id: str, o: Order) -> Order: await db.execute( f""" - INSERT INTO nostrmarket.orders (user_id, id, event_id, pubkey, address, contact_data, order_items, invoice_id, total) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + INSERT INTO nostrmarket.orders (user_id, id, event_id, pubkey, address, contact_data, order_items, stall_id, invoice_id, total) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( user_id, @@ -329,6 +329,7 @@ async def create_order(user_id: str, o: Order) -> Order: o.address, json.dumps(o.contact.dict() if o.contact else {}), json.dumps([i.dict() for i in o.items]), + o.stall_id, o.invoice_id, o.total, ), @@ -359,3 +360,22 @@ async def get_order_by_event_id(user_id: str, event_id: str) -> Optional[Order]: ), ) return Order.from_row(row) if row else None + + +async def get_orders(user_id: str) -> List[Order]: + rows = await db.fetchall( + "SELECT * FROM nostrmarket.orders WHERE user_id = ?", + (user_id,), + ) + return [Order.from_row(row) for row in rows] + + +async def get_orders_for_stall(user_id: str, stall_id: str) -> List[Order]: + rows = await db.fetchall( + "SELECT * FROM nostrmarket.orders WHERE user_id = ? AND stall_id = ?", + ( + user_id, + stall_id, + ), + ) + return [Order.from_row(row) for row in rows] diff --git a/migrations.py b/migrations.py index 4ed088b..7c50264 100644 --- a/migrations.py +++ b/migrations.py @@ -78,11 +78,12 @@ async def m001_initial(db): user_id TEXT NOT NULL, id TEXT PRIMARY KEY, event_id TEXT, - pubkey EXT NOT NULL, + pubkey TEXT NOT NULL, contact_data TEXT NOT NULL DEFAULT '{empty_object}', order_items TEXT NOT NULL, address TEXT, total REAL NOT NULL, + stall_id TEXT NOT NULL, invoice_id TEXT NOT NULL, paid BOOLEAN NOT NULL DEFAULT false, shipped BOOLEAN NOT NULL DEFAULT false, diff --git a/models.py b/models.py index 0cfee8f..a8415e1 100644 --- a/models.py +++ b/models.py @@ -269,6 +269,26 @@ class PartialOrder(BaseModel): def validate_order(self): assert len(self.items) != 0, f"Order has no items. Order: '{self.id}'" + def validate_order_items(self, product_list: List[Product]): + assert len(self.items) != 0, f"Order has no items. Order: '{self.id}'" + assert ( + len(product_list) != 0 + ), f"No products found for order. Order: '{self.id}'" + + product_ids = [p.id for p in product_list] + for item in self.items: + if item.product_id not in product_ids: + raise ValueError( + f"Order ({self.id}) item product does not exist: {item.product_id}" + ) + + stall_id = product_list[0].stall_id + for p in product_list: + if p.stall_id != stall_id: + raise ValueError( + f"Order ({self.id}) has products from different stalls" + ) + async def total_sats(self, products: List[Product]) -> float: product_prices = {} for p in products: @@ -286,10 +306,12 @@ class PartialOrder(BaseModel): class Order(PartialOrder): + stall_id: str invoice_id: str total: float paid: bool = False shipped: bool = False + time: int @classmethod def from_row(cls, row: Row) -> "Order": diff --git a/static/components/order-list/order-list.html b/static/components/order-list/order-list.html index 147c21f..99c556a 100644 --- a/static/components/order-list/order-list.html +++ b/static/components/order-list/order-list.html @@ -1,3 +1,44 @@
- xx1 -
\ No newline at end of file + + + + diff --git a/static/components/order-list/order-list.js b/static/components/order-list/order-list.js index 15b1528..9d5e1f7 100644 --- a/static/components/order-list/order-list.js +++ b/static/components/order-list/order-list.js @@ -1,18 +1,95 @@ async function orderList(path) { - const template = await loadTemplateAsync(path) - Vue.component('order-list', { - name: 'order-list', - props: ['adminkey', 'inkey'], - template, - - data: function () { - return { + const template = await loadTemplateAsync(path) + Vue.component('order-list', { + name: 'order-list', + props: ['stall-id', 'adminkey', 'inkey'], + template, + + data: function () { + return { + orders: [], + + filter: '', + ordersTable: { + columns: [ + { + name: '', + align: 'left', + label: '', + field: '' + }, + { + name: 'id', + align: 'left', + label: 'ID', + field: 'id' + }, + { + name: 'total', + align: 'left', + label: 'Total', + field: 'total' + }, + { + name: 'paid', + align: 'left', + label: 'Paid', + field: 'paid' + }, + { + name: 'shipped', + align: 'left', + label: 'Shipped', + field: 'shipped' + }, + { + name: 'pubkey', + align: 'left', + label: 'Customer', + field: 'pubkey' + }, + { + name: 'time', + align: 'left', + label: 'Date', + field: 'time' + } + ], + pagination: { + rowsPerPage: 10 + } } - }, - methods: { - }, - created: async function () { } - }) - } - \ No newline at end of file + }, + methods: { + toShortId: function (value) { + return value.substring(0, 5) + '...' + value.substring(value.length - 5) + }, + formatDate: function (value) { + return Quasar.utils.date.formatDate( + new Date(value * 1000), + 'YYYY-MM-DD HH:mm' + ) + }, + getOrders: async function () { + try { + const ordersPath = this.stallId + ? `/stall/order/${this.stallId}` + : '/order' + const {data} = await LNbits.api.request( + 'GET', + '/nostrmarket/api/v1' + ordersPath, + this.inkey + ) + this.orders = data.map(s => ({...s, expanded: false})) + console.log('### this.orders', this.orders) + } catch (error) { + LNbits.utils.notifyApiError(error) + } + } + }, + created: async function () { + await this.getOrders() + } + }) +} diff --git a/static/components/stall-details/stall-details.html b/static/components/stall-details/stall-details.html index 9ffe49a..4239f68 100644 --- a/static/components/stall-details/stall-details.html +++ b/static/components/stall-details/stall-details.html @@ -187,10 +187,10 @@
+ :adminkey="adminkey" + :inkey="inkey" + :stall-id="stallId" + >
diff --git a/static/components/stall-list/stall-list.html b/static/components/stall-list/stall-list.html index 86ae34e..bc6236e 100644 --- a/static/components/stall-list/stall-list.html +++ b/static/components/stall-list/stall-list.html @@ -50,7 +50,7 @@ {{props.row.name}}
- + {{props.row.currency}} {{props.row.config.description}} diff --git a/static/components/stall-list/stall-list.js b/static/components/stall-list/stall-list.js index 5fd8ffd..d41c062 100644 --- a/static/components/stall-list/stall-list.js +++ b/static/components/stall-list/stall-list.js @@ -35,6 +35,12 @@ async function stallList(path) { label: 'Name', field: 'id' }, + { + name: 'currency', + align: 'left', + label: 'Currency', + field: 'currency' + }, { name: 'description', align: 'left', diff --git a/tasks.py b/tasks.py index 9a78344..f4b9477 100644 --- a/tasks.py +++ b/tasks.py @@ -127,9 +127,6 @@ async def handle_new_order(order: PartialOrder): ### check that event_id not parsed already order.validate_order() - assert ( - len(order.items) != 0 - ), f"Order has no items. Order: '{order.id}' ({order.event_id})" first_product_id = order.items[0].product_id wallet_id = await get_wallet_for_product(first_product_id) diff --git a/views_api.py b/views_api.py index bca744d..e21193d 100644 --- a/views_api.py +++ b/views_api.py @@ -1,3 +1,4 @@ +import json from http import HTTPStatus from typing import List, Optional @@ -27,6 +28,8 @@ from .crud import ( 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, @@ -283,6 +286,22 @@ async def api_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) @@ -435,6 +454,7 @@ 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( @@ -445,6 +465,8 @@ async def api_create_order( 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) @@ -460,7 +482,12 @@ async def api_create_order( }, ) - order = Order(**data.dict(), invoice_id=payment_hash, total=total_amount) + order = Order( + **data.dict(), + stall_id=products[0].stall_id, + invoice_id=payment_hash, + total=total_amount, + ) await create_order(wallet.wallet.user, order) return PaymentRequest( @@ -474,6 +501,41 @@ async def api_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", + ) + + ######################################## OTHER ######################################## From fc02609d476f7e64acee115127f5519bb64aab54 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Tue, 7 Mar 2023 10:31:25 +0200 Subject: [PATCH 156/891] feat: show orders per stall --- crud.py | 24 +++- migrations.py | 3 +- models.py | 22 ++++ static/components/order-list/order-list.html | 45 +++++++- static/components/order-list/order-list.js | 107 +++++++++++++++--- .../stall-details/stall-details.html | 8 +- static/components/stall-list/stall-list.html | 2 +- static/components/stall-list/stall-list.js | 6 + tasks.py | 3 - views_api.py | 64 ++++++++++- 10 files changed, 255 insertions(+), 29 deletions(-) diff --git a/crud.py b/crud.py index c74512f..be080b7 100644 --- a/crud.py +++ b/crud.py @@ -318,8 +318,8 @@ async def delete_product(user_id: str, product_id: str) -> None: async def create_order(user_id: str, o: Order) -> Order: await db.execute( f""" - INSERT INTO nostrmarket.orders (user_id, id, event_id, pubkey, address, contact_data, order_items, invoice_id, total) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + INSERT INTO nostrmarket.orders (user_id, id, event_id, pubkey, address, contact_data, order_items, stall_id, invoice_id, total) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( user_id, @@ -329,6 +329,7 @@ async def create_order(user_id: str, o: Order) -> Order: o.address, json.dumps(o.contact.dict() if o.contact else {}), json.dumps([i.dict() for i in o.items]), + o.stall_id, o.invoice_id, o.total, ), @@ -359,3 +360,22 @@ async def get_order_by_event_id(user_id: str, event_id: str) -> Optional[Order]: ), ) return Order.from_row(row) if row else None + + +async def get_orders(user_id: str) -> List[Order]: + rows = await db.fetchall( + "SELECT * FROM nostrmarket.orders WHERE user_id = ?", + (user_id,), + ) + return [Order.from_row(row) for row in rows] + + +async def get_orders_for_stall(user_id: str, stall_id: str) -> List[Order]: + rows = await db.fetchall( + "SELECT * FROM nostrmarket.orders WHERE user_id = ? AND stall_id = ?", + ( + user_id, + stall_id, + ), + ) + return [Order.from_row(row) for row in rows] diff --git a/migrations.py b/migrations.py index 4ed088b..7c50264 100644 --- a/migrations.py +++ b/migrations.py @@ -78,11 +78,12 @@ async def m001_initial(db): user_id TEXT NOT NULL, id TEXT PRIMARY KEY, event_id TEXT, - pubkey EXT NOT NULL, + pubkey TEXT NOT NULL, contact_data TEXT NOT NULL DEFAULT '{empty_object}', order_items TEXT NOT NULL, address TEXT, total REAL NOT NULL, + stall_id TEXT NOT NULL, invoice_id TEXT NOT NULL, paid BOOLEAN NOT NULL DEFAULT false, shipped BOOLEAN NOT NULL DEFAULT false, diff --git a/models.py b/models.py index 0cfee8f..a8415e1 100644 --- a/models.py +++ b/models.py @@ -269,6 +269,26 @@ class PartialOrder(BaseModel): def validate_order(self): assert len(self.items) != 0, f"Order has no items. Order: '{self.id}'" + def validate_order_items(self, product_list: List[Product]): + assert len(self.items) != 0, f"Order has no items. Order: '{self.id}'" + assert ( + len(product_list) != 0 + ), f"No products found for order. Order: '{self.id}'" + + product_ids = [p.id for p in product_list] + for item in self.items: + if item.product_id not in product_ids: + raise ValueError( + f"Order ({self.id}) item product does not exist: {item.product_id}" + ) + + stall_id = product_list[0].stall_id + for p in product_list: + if p.stall_id != stall_id: + raise ValueError( + f"Order ({self.id}) has products from different stalls" + ) + async def total_sats(self, products: List[Product]) -> float: product_prices = {} for p in products: @@ -286,10 +306,12 @@ class PartialOrder(BaseModel): class Order(PartialOrder): + stall_id: str invoice_id: str total: float paid: bool = False shipped: bool = False + time: int @classmethod def from_row(cls, row: Row) -> "Order": diff --git a/static/components/order-list/order-list.html b/static/components/order-list/order-list.html index 147c21f..99c556a 100644 --- a/static/components/order-list/order-list.html +++ b/static/components/order-list/order-list.html @@ -1,3 +1,44 @@
- xx1 -
\ No newline at end of file + + + + diff --git a/static/components/order-list/order-list.js b/static/components/order-list/order-list.js index 15b1528..9d5e1f7 100644 --- a/static/components/order-list/order-list.js +++ b/static/components/order-list/order-list.js @@ -1,18 +1,95 @@ async function orderList(path) { - const template = await loadTemplateAsync(path) - Vue.component('order-list', { - name: 'order-list', - props: ['adminkey', 'inkey'], - template, - - data: function () { - return { + const template = await loadTemplateAsync(path) + Vue.component('order-list', { + name: 'order-list', + props: ['stall-id', 'adminkey', 'inkey'], + template, + + data: function () { + return { + orders: [], + + filter: '', + ordersTable: { + columns: [ + { + name: '', + align: 'left', + label: '', + field: '' + }, + { + name: 'id', + align: 'left', + label: 'ID', + field: 'id' + }, + { + name: 'total', + align: 'left', + label: 'Total', + field: 'total' + }, + { + name: 'paid', + align: 'left', + label: 'Paid', + field: 'paid' + }, + { + name: 'shipped', + align: 'left', + label: 'Shipped', + field: 'shipped' + }, + { + name: 'pubkey', + align: 'left', + label: 'Customer', + field: 'pubkey' + }, + { + name: 'time', + align: 'left', + label: 'Date', + field: 'time' + } + ], + pagination: { + rowsPerPage: 10 + } } - }, - methods: { - }, - created: async function () { } - }) - } - \ No newline at end of file + }, + methods: { + toShortId: function (value) { + return value.substring(0, 5) + '...' + value.substring(value.length - 5) + }, + formatDate: function (value) { + return Quasar.utils.date.formatDate( + new Date(value * 1000), + 'YYYY-MM-DD HH:mm' + ) + }, + getOrders: async function () { + try { + const ordersPath = this.stallId + ? `/stall/order/${this.stallId}` + : '/order' + const {data} = await LNbits.api.request( + 'GET', + '/nostrmarket/api/v1' + ordersPath, + this.inkey + ) + this.orders = data.map(s => ({...s, expanded: false})) + console.log('### this.orders', this.orders) + } catch (error) { + LNbits.utils.notifyApiError(error) + } + } + }, + created: async function () { + await this.getOrders() + } + }) +} diff --git a/static/components/stall-details/stall-details.html b/static/components/stall-details/stall-details.html index 9ffe49a..4239f68 100644 --- a/static/components/stall-details/stall-details.html +++ b/static/components/stall-details/stall-details.html @@ -187,10 +187,10 @@
+ :adminkey="adminkey" + :inkey="inkey" + :stall-id="stallId" + >
diff --git a/static/components/stall-list/stall-list.html b/static/components/stall-list/stall-list.html index 86ae34e..bc6236e 100644 --- a/static/components/stall-list/stall-list.html +++ b/static/components/stall-list/stall-list.html @@ -50,7 +50,7 @@ {{props.row.name}} - + {{props.row.currency}} {{props.row.config.description}} diff --git a/static/components/stall-list/stall-list.js b/static/components/stall-list/stall-list.js index 5fd8ffd..d41c062 100644 --- a/static/components/stall-list/stall-list.js +++ b/static/components/stall-list/stall-list.js @@ -35,6 +35,12 @@ async function stallList(path) { label: 'Name', field: 'id' }, + { + name: 'currency', + align: 'left', + label: 'Currency', + field: 'currency' + }, { name: 'description', align: 'left', diff --git a/tasks.py b/tasks.py index 9a78344..f4b9477 100644 --- a/tasks.py +++ b/tasks.py @@ -127,9 +127,6 @@ async def handle_new_order(order: PartialOrder): ### check that event_id not parsed already order.validate_order() - assert ( - len(order.items) != 0 - ), f"Order has no items. Order: '{order.id}' ({order.event_id})" first_product_id = order.items[0].product_id wallet_id = await get_wallet_for_product(first_product_id) diff --git a/views_api.py b/views_api.py index bca744d..e21193d 100644 --- a/views_api.py +++ b/views_api.py @@ -1,3 +1,4 @@ +import json from http import HTTPStatus from typing import List, Optional @@ -27,6 +28,8 @@ from .crud import ( 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, @@ -283,6 +286,22 @@ async def api_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) @@ -435,6 +454,7 @@ 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( @@ -445,6 +465,8 @@ async def api_create_order( 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) @@ -460,7 +482,12 @@ async def api_create_order( }, ) - order = Order(**data.dict(), invoice_id=payment_hash, total=total_amount) + order = Order( + **data.dict(), + stall_id=products[0].stall_id, + invoice_id=payment_hash, + total=total_amount, + ) await create_order(wallet.wallet.user, order) return PaymentRequest( @@ -474,6 +501,41 @@ async def api_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", + ) + + ######################################## OTHER ######################################## From aa7e30e5f9434ec89b8ebb8ff2b2759cd3687533 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Tue, 7 Mar 2023 10:40:00 +0200 Subject: [PATCH 157/891] feat: show `paid` and `shipped` statues --- crud.py | 4 ++-- static/components/order-list/order-list.html | 19 +++++++++++++++++-- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/crud.py b/crud.py index be080b7..62e862f 100644 --- a/crud.py +++ b/crud.py @@ -364,7 +364,7 @@ async def get_order_by_event_id(user_id: str, event_id: str) -> Optional[Order]: async def get_orders(user_id: str) -> List[Order]: rows = await db.fetchall( - "SELECT * FROM nostrmarket.orders WHERE user_id = ?", + "SELECT * FROM nostrmarket.orders WHERE user_id = ? ORDER BY time DESC", (user_id,), ) return [Order.from_row(row) for row in rows] @@ -372,7 +372,7 @@ async def get_orders(user_id: str) -> List[Order]: async def get_orders_for_stall(user_id: str, stall_id: str) -> List[Order]: rows = await db.fetchall( - "SELECT * FROM nostrmarket.orders WHERE user_id = ? AND stall_id = ?", + "SELECT * FROM nostrmarket.orders WHERE user_id = ? AND stall_id = ? ORDER BY time DESC", ( user_id, stall_id, diff --git a/static/components/order-list/order-list.html b/static/components/order-list/order-list.html index 99c556a..2f086fa 100644 --- a/static/components/order-list/order-list.html +++ b/static/components/order-list/order-list.html @@ -23,9 +23,24 @@ {{toShortId(props.row.id)}} {{props.row.total}} + - {{props.row.paid}} - {{props.row.shipped}} + + + + + + {{toShortId(props.row.pubkey)}} From c0fef1bd1c09e3a37e2ed281d146f60e72eaa932 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Tue, 7 Mar 2023 10:40:00 +0200 Subject: [PATCH 158/891] feat: show `paid` and `shipped` statues --- crud.py | 4 ++-- static/components/order-list/order-list.html | 19 +++++++++++++++++-- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/crud.py b/crud.py index be080b7..62e862f 100644 --- a/crud.py +++ b/crud.py @@ -364,7 +364,7 @@ async def get_order_by_event_id(user_id: str, event_id: str) -> Optional[Order]: async def get_orders(user_id: str) -> List[Order]: rows = await db.fetchall( - "SELECT * FROM nostrmarket.orders WHERE user_id = ?", + "SELECT * FROM nostrmarket.orders WHERE user_id = ? ORDER BY time DESC", (user_id,), ) return [Order.from_row(row) for row in rows] @@ -372,7 +372,7 @@ async def get_orders(user_id: str) -> List[Order]: async def get_orders_for_stall(user_id: str, stall_id: str) -> List[Order]: rows = await db.fetchall( - "SELECT * FROM nostrmarket.orders WHERE user_id = ? AND stall_id = ?", + "SELECT * FROM nostrmarket.orders WHERE user_id = ? AND stall_id = ? ORDER BY time DESC", ( user_id, stall_id, diff --git a/static/components/order-list/order-list.html b/static/components/order-list/order-list.html index 99c556a..2f086fa 100644 --- a/static/components/order-list/order-list.html +++ b/static/components/order-list/order-list.html @@ -23,9 +23,24 @@ {{toShortId(props.row.id)}} {{props.row.total}} + - {{props.row.paid}} - {{props.row.shipped}} + + + + + + {{toShortId(props.row.pubkey)}} From 68795d2db26154dc69a4fe89833513541e55c055 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Tue, 7 Mar 2023 11:02:49 +0200 Subject: [PATCH 159/891] feat: show order details --- static/components/order-list/order-list.html | 112 ++++++++++++++++++- 1 file changed, 108 insertions(+), 4 deletions(-) diff --git a/static/components/order-list/order-list.html b/static/components/order-list/order-list.html index 2f086fa..f1ebbb3 100644 --- a/static/components/order-list/order-list.html +++ b/static/components/order-list/order-list.html @@ -34,8 +34,8 @@ size="sm" > - - + -
-
+
+
Order ID:
+
+ +
+
+
+
+
Customer Public Key:
+
+ +
+
+
+
+
Address:
+
+ +
+
+
+
+
Nostr Contact Pubkey:
+
+ +
+
+
+
+
Phone:
+
+ +
+
+
+
+
Email:
+
+ +
+
+
+
+
Invoice ID:
+
+ +
+
From 51806590c14d4bd0863d7e73216d21a72a9d5091 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Tue, 7 Mar 2023 11:02:49 +0200 Subject: [PATCH 160/891] feat: show order details --- static/components/order-list/order-list.html | 112 ++++++++++++++++++- 1 file changed, 108 insertions(+), 4 deletions(-) diff --git a/static/components/order-list/order-list.html b/static/components/order-list/order-list.html index 2f086fa..f1ebbb3 100644 --- a/static/components/order-list/order-list.html +++ b/static/components/order-list/order-list.html @@ -34,8 +34,8 @@ size="sm" > - - + -
-
+
+
Order ID:
+
+ +
+
+
+
+
Customer Public Key:
+
+ +
+
+
+
+
Address:
+
+ +
+
+
+
+
Nostr Contact Pubkey:
+
+ +
+
+
+
+
Phone:
+
+ +
+
+
+
+
Email:
+
+ +
+
+
+
+
Invoice ID:
+
+ +
+
From 76af65c148b2e548d8cf1e44be2c80bb00f78f22 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Tue, 7 Mar 2023 12:00:43 +0200 Subject: [PATCH 161/891] feat: show more details about the order --- crud.py | 5 +- migrations.py | 1 + models.py | 28 +++++++++-- static/components/order-list/order-list.html | 52 ++++++++++++-------- static/components/order-list/order-list.js | 7 +++ tasks.py | 4 +- views_api.py | 2 + 7 files changed, 72 insertions(+), 27 deletions(-) diff --git a/crud.py b/crud.py index 62e862f..4297f7a 100644 --- a/crud.py +++ b/crud.py @@ -318,8 +318,8 @@ async def delete_product(user_id: str, product_id: str) -> None: async def create_order(user_id: str, o: Order) -> Order: await db.execute( f""" - INSERT INTO nostrmarket.orders (user_id, id, event_id, pubkey, address, contact_data, order_items, stall_id, invoice_id, total) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + INSERT INTO nostrmarket.orders (user_id, id, event_id, pubkey, address, contact_data, extra_data, order_items, stall_id, invoice_id, total) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( user_id, @@ -328,6 +328,7 @@ async def create_order(user_id: str, o: Order) -> Order: o.pubkey, o.address, json.dumps(o.contact.dict() if o.contact else {}), + json.dumps(o.extra.dict()), json.dumps([i.dict() for i in o.items]), o.stall_id, o.invoice_id, diff --git a/migrations.py b/migrations.py index 7c50264..f006d36 100644 --- a/migrations.py +++ b/migrations.py @@ -80,6 +80,7 @@ async def m001_initial(db): event_id TEXT, pubkey TEXT NOT NULL, contact_data TEXT NOT NULL DEFAULT '{empty_object}', + extra_data TEXT NOT NULL DEFAULT '{empty_object}', order_items TEXT NOT NULL, address TEXT, total REAL NOT NULL, diff --git a/models.py b/models.py index a8415e1..3c1111f 100644 --- a/models.py +++ b/models.py @@ -6,7 +6,7 @@ from typing import List, Optional from pydantic import BaseModel -from lnbits.utils.exchange_rates import fiat_amount_as_satoshis +from lnbits.utils.exchange_rates import btc_price, fiat_amount_as_satoshis from .helpers import ( decrypt_message, @@ -244,6 +244,12 @@ class Product(PartialProduct, Nostrable): return product +class ProductOverview(BaseModel): + id: str + name: str + price: float + + ######################################## ORDERS ######################################## @@ -258,6 +264,20 @@ class OrderContact(BaseModel): email: Optional[str] +class OrderExtra(BaseModel): + products: List[ProductOverview] + currency: str + btc_price: str + + @classmethod + async def from_products(cls, products: List[Product]): + currency = products[0].config.currency + exchange_rate = ( + (await btc_price(currency)) if currency and currency != "sat" else 1 + ) + return OrderExtra(products=products, currency=currency, btc_price=exchange_rate) + + class PartialOrder(BaseModel): id: str event_id: Optional[str] @@ -311,13 +331,15 @@ class Order(PartialOrder): total: float paid: bool = False shipped: bool = False - time: int + time: Optional[int] + extra: OrderExtra @classmethod def from_row(cls, row: Row) -> "Order": contact = OrderContact(**json.loads(row["contact_data"])) + extra = OrderExtra(**json.loads(row["extra_data"])) items = [OrderItem(**z) for z in json.loads(row["order_items"])] - order = cls(**dict(row), contact=contact, items=items) + order = cls(**dict(row), contact=contact, items=items, extra=extra) return order diff --git a/static/components/order-list/order-list.html b/static/components/order-list/order-list.html index f1ebbb3..a0968e7 100644 --- a/static/components/order-list/order-list.html +++ b/static/components/order-list/order-list.html @@ -63,20 +63,7 @@
-
-
Customer Public Key:
-
- -
-
-
+
Address:
@@ -91,23 +78,48 @@
-
-
Nostr Contact Pubkey:
+
+
Products:
+
+
+
Quantity
+
+
Name
+
+
+
+
+
+
+
+
+
{{item.quantity}}
+
x
+
+ {{productOverview(props.row, item.product_id)}} +
+
+
+
+
+
+
Customer Public Key:
+
p.id === productId) + if (product) { + return `${product.name} (${product.price} ${order.extra.currency})` + } + return '' + }, getOrders: async function () { try { const ordersPath = this.stallId diff --git a/tasks.py b/tasks.py index f4b9477..69df5a3 100644 --- a/tasks.py +++ b/tasks.py @@ -36,7 +36,7 @@ async def on_invoice_paid(payment: Payment) -> None: if payment.extra.get("tag") != "nostrmarket": return - print("### on_invoice_paid") + print("### on_invoice_paid", json.dumps(payment)) async def subscribe_to_nostr_client(recieve_event_queue: Queue, send_req_queue: Queue): @@ -124,7 +124,7 @@ async def handle_dirrect_message( async def handle_new_order(order: PartialOrder): - ### check that event_id not parsed already + ### todo: check that event_id not parsed already order.validate_order() diff --git a/views_api.py b/views_api.py index e21193d..48935aa 100644 --- a/views_api.py +++ b/views_api.py @@ -46,6 +46,7 @@ from .models import ( Merchant, Nostrable, Order, + OrderExtra, PartialMerchant, PartialOrder, PartialProduct, @@ -487,6 +488,7 @@ async def api_create_order( 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) From f98e66038d82b055c9c9b617695fa937220c59fd Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Tue, 7 Mar 2023 12:00:43 +0200 Subject: [PATCH 162/891] feat: show more details about the order --- crud.py | 5 +- migrations.py | 1 + models.py | 28 +++++++++-- static/components/order-list/order-list.html | 52 ++++++++++++-------- static/components/order-list/order-list.js | 7 +++ tasks.py | 4 +- views_api.py | 2 + 7 files changed, 72 insertions(+), 27 deletions(-) diff --git a/crud.py b/crud.py index 62e862f..4297f7a 100644 --- a/crud.py +++ b/crud.py @@ -318,8 +318,8 @@ async def delete_product(user_id: str, product_id: str) -> None: async def create_order(user_id: str, o: Order) -> Order: await db.execute( f""" - INSERT INTO nostrmarket.orders (user_id, id, event_id, pubkey, address, contact_data, order_items, stall_id, invoice_id, total) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + INSERT INTO nostrmarket.orders (user_id, id, event_id, pubkey, address, contact_data, extra_data, order_items, stall_id, invoice_id, total) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( user_id, @@ -328,6 +328,7 @@ async def create_order(user_id: str, o: Order) -> Order: o.pubkey, o.address, json.dumps(o.contact.dict() if o.contact else {}), + json.dumps(o.extra.dict()), json.dumps([i.dict() for i in o.items]), o.stall_id, o.invoice_id, diff --git a/migrations.py b/migrations.py index 7c50264..f006d36 100644 --- a/migrations.py +++ b/migrations.py @@ -80,6 +80,7 @@ async def m001_initial(db): event_id TEXT, pubkey TEXT NOT NULL, contact_data TEXT NOT NULL DEFAULT '{empty_object}', + extra_data TEXT NOT NULL DEFAULT '{empty_object}', order_items TEXT NOT NULL, address TEXT, total REAL NOT NULL, diff --git a/models.py b/models.py index a8415e1..3c1111f 100644 --- a/models.py +++ b/models.py @@ -6,7 +6,7 @@ from typing import List, Optional from pydantic import BaseModel -from lnbits.utils.exchange_rates import fiat_amount_as_satoshis +from lnbits.utils.exchange_rates import btc_price, fiat_amount_as_satoshis from .helpers import ( decrypt_message, @@ -244,6 +244,12 @@ class Product(PartialProduct, Nostrable): return product +class ProductOverview(BaseModel): + id: str + name: str + price: float + + ######################################## ORDERS ######################################## @@ -258,6 +264,20 @@ class OrderContact(BaseModel): email: Optional[str] +class OrderExtra(BaseModel): + products: List[ProductOverview] + currency: str + btc_price: str + + @classmethod + async def from_products(cls, products: List[Product]): + currency = products[0].config.currency + exchange_rate = ( + (await btc_price(currency)) if currency and currency != "sat" else 1 + ) + return OrderExtra(products=products, currency=currency, btc_price=exchange_rate) + + class PartialOrder(BaseModel): id: str event_id: Optional[str] @@ -311,13 +331,15 @@ class Order(PartialOrder): total: float paid: bool = False shipped: bool = False - time: int + time: Optional[int] + extra: OrderExtra @classmethod def from_row(cls, row: Row) -> "Order": contact = OrderContact(**json.loads(row["contact_data"])) + extra = OrderExtra(**json.loads(row["extra_data"])) items = [OrderItem(**z) for z in json.loads(row["order_items"])] - order = cls(**dict(row), contact=contact, items=items) + order = cls(**dict(row), contact=contact, items=items, extra=extra) return order diff --git a/static/components/order-list/order-list.html b/static/components/order-list/order-list.html index f1ebbb3..a0968e7 100644 --- a/static/components/order-list/order-list.html +++ b/static/components/order-list/order-list.html @@ -63,20 +63,7 @@
-
-
Customer Public Key:
-
- -
-
-
+
Address:
@@ -91,23 +78,48 @@
-
-
Nostr Contact Pubkey:
+
+
Products:
+
+
+
Quantity
+
+
Name
+
+
+
+
+
+
+
+
+
{{item.quantity}}
+
x
+
+ {{productOverview(props.row, item.product_id)}} +
+
+
+
+
+
+
Customer Public Key:
+
p.id === productId) + if (product) { + return `${product.name} (${product.price} ${order.extra.currency})` + } + return '' + }, getOrders: async function () { try { const ordersPath = this.stallId diff --git a/tasks.py b/tasks.py index f4b9477..69df5a3 100644 --- a/tasks.py +++ b/tasks.py @@ -36,7 +36,7 @@ async def on_invoice_paid(payment: Payment) -> None: if payment.extra.get("tag") != "nostrmarket": return - print("### on_invoice_paid") + print("### on_invoice_paid", json.dumps(payment)) async def subscribe_to_nostr_client(recieve_event_queue: Queue, send_req_queue: Queue): @@ -124,7 +124,7 @@ async def handle_dirrect_message( async def handle_new_order(order: PartialOrder): - ### check that event_id not parsed already + ### todo: check that event_id not parsed already order.validate_order() diff --git a/views_api.py b/views_api.py index e21193d..48935aa 100644 --- a/views_api.py +++ b/views_api.py @@ -46,6 +46,7 @@ from .models import ( Merchant, Nostrable, Order, + OrderExtra, PartialMerchant, PartialOrder, PartialProduct, @@ -487,6 +488,7 @@ async def api_create_order( 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) From 8094bcaf8ac2d882ebc0e96ae97a37475834f497 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Tue, 7 Mar 2023 13:10:01 +0200 Subject: [PATCH 163/891] feat: handle order paid --- crud.py | 27 +++++++++++++++++++++++++-- models.py | 7 +++++++ tasks.py | 38 ++++++++++++++++++++++++++++++++------ views_api.py | 34 +++++++++++++++++++++++++++++----- 4 files changed, 93 insertions(+), 13 deletions(-) diff --git a/crud.py b/crud.py index 4297f7a..b8ee099 100644 --- a/crud.py +++ b/crud.py @@ -1,5 +1,4 @@ import json -import time from typing import List, Optional from lnbits.helpers import urlsafe_short_hash @@ -9,7 +8,6 @@ from .models import ( Merchant, Order, PartialMerchant, - PartialOrder, PartialProduct, PartialStall, PartialZone, @@ -380,3 +378,28 @@ async def get_orders_for_stall(user_id: str, stall_id: str) -> List[Order]: ), ) return [Order.from_row(row) for row in rows] + + +async def update_order_paid_status(order_id: str, paid: bool) -> Optional[Order]: + await db.execute( + f"UPDATE nostrmarket.orders SET paid = ? WHERE id = ?", + (paid, order_id), + ) + row = await db.fetchone( + "SELECT * FROM nostrmarket.orders WHERE id = ?", + (order_id,), + ) + return Order.from_row(row) if row else None + + +async def update_order_shipped_status(order_id: str, shipped: bool) -> Optional[Order]: + await db.execute( + f"UPDATE nostrmarket.orders SET shipped = ? WHERE id = ?", + (order_id, shipped), + ) + + row = await db.fetchone( + "SELECT * FROM nostrmarket.orders WHERE id = ?", + (order_id,), + ) + return Order.from_row(row) if row else None diff --git a/models.py b/models.py index 3c1111f..f228ed5 100644 --- a/models.py +++ b/models.py @@ -343,6 +343,13 @@ class Order(PartialOrder): return order +class OrderStatusUpdate(BaseModel): + id: str + message: Optional[str] + paid: Optional[bool] + shipped: Optional[bool] + + class PaymentOption(BaseModel): type: str link: str diff --git a/tasks.py b/tasks.py index 69df5a3..f5afec3 100644 --- a/tasks.py +++ b/tasks.py @@ -16,9 +16,10 @@ from .crud import ( get_merchant_by_pubkey, get_public_keys_for_merchants, get_wallet_for_product, + update_order_paid_status, ) from .helpers import order_from_json -from .models import PartialOrder +from .models import OrderStatusUpdate, PartialOrder from .nostr.event import NostrEvent from .nostr.nostr_client import connect_to_nostrclient_ws, publish_nostr_event @@ -36,7 +37,33 @@ async def on_invoice_paid(payment: Payment) -> None: if payment.extra.get("tag") != "nostrmarket": return - print("### on_invoice_paid", json.dumps(payment)) + order_id = payment.extra.get("order_id") + merchant_pubkey = payment.extra.get("merchant_pubkey") + if not order_id or not merchant_pubkey: + return None + + await handle_order_paid(order_id, merchant_pubkey) + + + +async def handle_order_paid(order_id: str, merchant_pubkey: str): + try: + order = await update_order_paid_status(order_id, True) + assert order, f"Paid order cannot be found. Order id: {order_id}" + order_status = OrderStatusUpdate( + id=order_id, message="Payment received.", paid=True, shipped=order.shipped + ) + + merchant = await get_merchant_by_pubkey(merchant_pubkey) + assert merchant, f"Merchant cannot be foud for order {order_id}" + dm_content = json.dumps( + order_status.dict(), separators=(",", ":"), ensure_ascii=False + ) + + dm_event = merchant.build_dm_event(dm_content, order.pubkey) + await publish_nostr_event(dm_event) + except Exception as ex: + logger.warning(ex) async def subscribe_to_nostr_client(recieve_event_queue: Queue, send_req_queue: Queue): @@ -60,7 +87,6 @@ async def subscribe_to_nostr_client(recieve_event_queue: Queue, send_req_queue: # be sure the connection is open await asyncio.sleep(3) req = await send_req_queue.get() - print("### req", req) ws.send(json.dumps(req)) except Exception as ex: logger.warning(ex) @@ -100,9 +126,9 @@ async def handle_nip04_message(public_key: str, event: NostrEvent): assert merchant, f"Merchant not found for public key '{public_key}'" clear_text_msg = merchant.decrypt_message(event.content, event.pubkey) - dm_resp = await handle_dirrect_message(event.pubkey, event.id, clear_text_msg) - if dm_resp: - dm_event = merchant.build_dm_event(dm_resp, event.pubkey) + dm_content = await handle_dirrect_message(event.pubkey, event.id, clear_text_msg) + if dm_content: + dm_event = merchant.build_dm_event(dm_content, event.pubkey) await publish_nostr_event(dm_event) diff --git a/views_api.py b/views_api.py index 48935aa..32696e4 100644 --- a/views_api.py +++ b/views_api.py @@ -93,7 +93,7 @@ async def api_get_merchant( logger.warning(ex) raise HTTPException( status_code=HTTPStatus.INTERNAL_SERVER_ERROR, - detail="Cannot create merchant", + detail="Cannot get merchant", ) @@ -108,7 +108,7 @@ async def api_get_zones(wallet: WalletTypeInfo = Depends(get_key_type)) -> List[ logger.warning(ex) raise HTTPException( status_code=HTTPStatus.INTERNAL_SERVER_ERROR, - detail="Cannot create merchant", + detail="Cannot get zone", ) @@ -123,7 +123,7 @@ async def api_create_zone( logger.warning(ex) raise HTTPException( status_code=HTTPStatus.INTERNAL_SERVER_ERROR, - detail="Cannot create merchant", + detail="Cannot create zone", ) @@ -149,7 +149,7 @@ async def api_update_zone( logger.warning(ex) raise HTTPException( status_code=HTTPStatus.INTERNAL_SERVER_ERROR, - detail="Cannot create merchant", + detail="Cannot update zone", ) @@ -170,7 +170,7 @@ async def api_delete_zone(zone_id, wallet: WalletTypeInfo = Depends(require_admi logger.warning(ex) raise HTTPException( status_code=HTTPStatus.INTERNAL_SERVER_ERROR, - detail="Cannot create merchant", + detail="Cannot delete zone", ) @@ -463,6 +463,9 @@ async def api_create_order( ): 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] ) @@ -480,6 +483,7 @@ async def api_create_order( extra={ "tag": "nostrmarket", "order_id": data.id, + "merchant_pubkey": merchant.public_key, }, ) @@ -538,6 +542,26 @@ async def api_get_orders(wallet: WalletTypeInfo = Depends(get_key_type)): ) +# @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 ######################################## From aa0d662a4f87f04b3cda8108c6d67ae497a91cdb Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Tue, 7 Mar 2023 13:10:01 +0200 Subject: [PATCH 164/891] feat: handle order paid --- crud.py | 27 +++++++++++++++++++++++++-- models.py | 7 +++++++ tasks.py | 38 ++++++++++++++++++++++++++++++++------ views_api.py | 34 +++++++++++++++++++++++++++++----- 4 files changed, 93 insertions(+), 13 deletions(-) diff --git a/crud.py b/crud.py index 4297f7a..b8ee099 100644 --- a/crud.py +++ b/crud.py @@ -1,5 +1,4 @@ import json -import time from typing import List, Optional from lnbits.helpers import urlsafe_short_hash @@ -9,7 +8,6 @@ from .models import ( Merchant, Order, PartialMerchant, - PartialOrder, PartialProduct, PartialStall, PartialZone, @@ -380,3 +378,28 @@ async def get_orders_for_stall(user_id: str, stall_id: str) -> List[Order]: ), ) return [Order.from_row(row) for row in rows] + + +async def update_order_paid_status(order_id: str, paid: bool) -> Optional[Order]: + await db.execute( + f"UPDATE nostrmarket.orders SET paid = ? WHERE id = ?", + (paid, order_id), + ) + row = await db.fetchone( + "SELECT * FROM nostrmarket.orders WHERE id = ?", + (order_id,), + ) + return Order.from_row(row) if row else None + + +async def update_order_shipped_status(order_id: str, shipped: bool) -> Optional[Order]: + await db.execute( + f"UPDATE nostrmarket.orders SET shipped = ? WHERE id = ?", + (order_id, shipped), + ) + + row = await db.fetchone( + "SELECT * FROM nostrmarket.orders WHERE id = ?", + (order_id,), + ) + return Order.from_row(row) if row else None diff --git a/models.py b/models.py index 3c1111f..f228ed5 100644 --- a/models.py +++ b/models.py @@ -343,6 +343,13 @@ class Order(PartialOrder): return order +class OrderStatusUpdate(BaseModel): + id: str + message: Optional[str] + paid: Optional[bool] + shipped: Optional[bool] + + class PaymentOption(BaseModel): type: str link: str diff --git a/tasks.py b/tasks.py index 69df5a3..f5afec3 100644 --- a/tasks.py +++ b/tasks.py @@ -16,9 +16,10 @@ from .crud import ( get_merchant_by_pubkey, get_public_keys_for_merchants, get_wallet_for_product, + update_order_paid_status, ) from .helpers import order_from_json -from .models import PartialOrder +from .models import OrderStatusUpdate, PartialOrder from .nostr.event import NostrEvent from .nostr.nostr_client import connect_to_nostrclient_ws, publish_nostr_event @@ -36,7 +37,33 @@ async def on_invoice_paid(payment: Payment) -> None: if payment.extra.get("tag") != "nostrmarket": return - print("### on_invoice_paid", json.dumps(payment)) + order_id = payment.extra.get("order_id") + merchant_pubkey = payment.extra.get("merchant_pubkey") + if not order_id or not merchant_pubkey: + return None + + await handle_order_paid(order_id, merchant_pubkey) + + + +async def handle_order_paid(order_id: str, merchant_pubkey: str): + try: + order = await update_order_paid_status(order_id, True) + assert order, f"Paid order cannot be found. Order id: {order_id}" + order_status = OrderStatusUpdate( + id=order_id, message="Payment received.", paid=True, shipped=order.shipped + ) + + merchant = await get_merchant_by_pubkey(merchant_pubkey) + assert merchant, f"Merchant cannot be foud for order {order_id}" + dm_content = json.dumps( + order_status.dict(), separators=(",", ":"), ensure_ascii=False + ) + + dm_event = merchant.build_dm_event(dm_content, order.pubkey) + await publish_nostr_event(dm_event) + except Exception as ex: + logger.warning(ex) async def subscribe_to_nostr_client(recieve_event_queue: Queue, send_req_queue: Queue): @@ -60,7 +87,6 @@ async def subscribe_to_nostr_client(recieve_event_queue: Queue, send_req_queue: # be sure the connection is open await asyncio.sleep(3) req = await send_req_queue.get() - print("### req", req) ws.send(json.dumps(req)) except Exception as ex: logger.warning(ex) @@ -100,9 +126,9 @@ async def handle_nip04_message(public_key: str, event: NostrEvent): assert merchant, f"Merchant not found for public key '{public_key}'" clear_text_msg = merchant.decrypt_message(event.content, event.pubkey) - dm_resp = await handle_dirrect_message(event.pubkey, event.id, clear_text_msg) - if dm_resp: - dm_event = merchant.build_dm_event(dm_resp, event.pubkey) + dm_content = await handle_dirrect_message(event.pubkey, event.id, clear_text_msg) + if dm_content: + dm_event = merchant.build_dm_event(dm_content, event.pubkey) await publish_nostr_event(dm_event) diff --git a/views_api.py b/views_api.py index 48935aa..32696e4 100644 --- a/views_api.py +++ b/views_api.py @@ -93,7 +93,7 @@ async def api_get_merchant( logger.warning(ex) raise HTTPException( status_code=HTTPStatus.INTERNAL_SERVER_ERROR, - detail="Cannot create merchant", + detail="Cannot get merchant", ) @@ -108,7 +108,7 @@ async def api_get_zones(wallet: WalletTypeInfo = Depends(get_key_type)) -> List[ logger.warning(ex) raise HTTPException( status_code=HTTPStatus.INTERNAL_SERVER_ERROR, - detail="Cannot create merchant", + detail="Cannot get zone", ) @@ -123,7 +123,7 @@ async def api_create_zone( logger.warning(ex) raise HTTPException( status_code=HTTPStatus.INTERNAL_SERVER_ERROR, - detail="Cannot create merchant", + detail="Cannot create zone", ) @@ -149,7 +149,7 @@ async def api_update_zone( logger.warning(ex) raise HTTPException( status_code=HTTPStatus.INTERNAL_SERVER_ERROR, - detail="Cannot create merchant", + detail="Cannot update zone", ) @@ -170,7 +170,7 @@ async def api_delete_zone(zone_id, wallet: WalletTypeInfo = Depends(require_admi logger.warning(ex) raise HTTPException( status_code=HTTPStatus.INTERNAL_SERVER_ERROR, - detail="Cannot create merchant", + detail="Cannot delete zone", ) @@ -463,6 +463,9 @@ async def api_create_order( ): 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] ) @@ -480,6 +483,7 @@ async def api_create_order( extra={ "tag": "nostrmarket", "order_id": data.id, + "merchant_pubkey": merchant.public_key, }, ) @@ -538,6 +542,26 @@ async def api_get_orders(wallet: WalletTypeInfo = Depends(get_key_type)): ) +# @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 ######################################## From 612d31eae07dc4998e300b700c47a43427e46cd1 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Tue, 7 Mar 2023 14:30:58 +0200 Subject: [PATCH 165/891] feat: add ship/unship functionality --- crud.py | 8 +- static/components/order-list/order-list.html | 84 +++++++++++++------- static/components/order-list/order-list.js | 37 ++++++++- tasks.py | 3 +- views_api.py | 47 +++++++---- 5 files changed, 129 insertions(+), 50 deletions(-) diff --git a/crud.py b/crud.py index b8ee099..2e6013b 100644 --- a/crud.py +++ b/crud.py @@ -392,10 +392,12 @@ async def update_order_paid_status(order_id: str, paid: bool) -> Optional[Order] return Order.from_row(row) if row else None -async def update_order_shipped_status(order_id: str, shipped: bool) -> Optional[Order]: +async def update_order_shipped_status( + user_id: str, order_id: str, shipped: bool +) -> Optional[Order]: await db.execute( - f"UPDATE nostrmarket.orders SET shipped = ? WHERE id = ?", - (order_id, shipped), + f"UPDATE nostrmarket.orders SET shipped = ? WHERE user_id = ? AND id = ?", + (shipped, user_id, order_id), ) row = await db.fetchone( diff --git a/static/components/order-list/order-list.html b/static/components/order-list/order-list.html index a0968e7..e629b1f 100644 --- a/static/components/order-list/order-list.html +++ b/static/components/order-list/order-list.html @@ -37,6 +37,7 @@ @@ -49,6 +50,33 @@ +
+
Products:
+
+
+
Quantity
+
+
Name
+
+
+
+
+
+
+
+
+
{{item.quantity}}
+
x
+
+ {{productOverview(props.row, item.product_id)}} +
+
+
+
+
Order ID:
@@ -78,33 +106,7 @@
-
-
Products:
-
-
-
Quantity
-
-
Name
-
-
-
-
-
-
-
-
-
{{item.quantity}}
-
x
-
- {{productOverview(props.row, item.product_id)}} -
-
-
-
-
+
Customer Public Key:
@@ -172,4 +174,32 @@ + + + + + + +
+ + + Cancel +
+
+
+
diff --git a/static/components/order-list/order-list.js b/static/components/order-list/order-list.js index bb4f335..482cc8a 100644 --- a/static/components/order-list/order-list.js +++ b/static/components/order-list/order-list.js @@ -8,7 +8,9 @@ async function orderList(path) { data: function () { return { orders: [], - + selectedOrder: null, + shippingMessage: '', + showShipDialog: false, filter: '', ordersTable: { columns: [ @@ -93,6 +95,39 @@ async function orderList(path) { } catch (error) { LNbits.utils.notifyApiError(error) } + }, + updateOrderShipped: async function () { + console.log('### order', this.selectedOrder) + this.selectedOrder.shipped = !this.selectedOrder.shipped + try { + await LNbits.api.request( + 'PATCH', + `/nostrmarket/api/v1/order/${this.selectedOrder.id}`, + this.adminkey, + { + id: this.selectedOrder.id, + message: this.shippingMessage, + shipped: this.selectedOrder.shipped + } + ) + this.$q.notify({ + type: 'positive', + message: 'Order updated!' + }) + } catch (error) { + LNbits.utils.notifyApiError(error) + } + this.showShipDialog = false + }, + showShipOrderDialog: function (order) { + this.selectedOrder = order + this.shippingMessage = order.shipped + ? `The order has been shipped! Order ID: '${order.id}' ` + : `The order has NOT yet been shipped! Order ID: '${order.id}'` + + // do not change the status yet + this.selectedOrder.shipped = !order.shipped + this.showShipDialog = true } }, created: async function () { diff --git a/tasks.py b/tasks.py index f5afec3..c97c2b2 100644 --- a/tasks.py +++ b/tasks.py @@ -45,7 +45,6 @@ async def on_invoice_paid(payment: Payment) -> None: await handle_order_paid(order_id, merchant_pubkey) - async def handle_order_paid(order_id: str, merchant_pubkey: str): try: order = await update_order_paid_status(order_id, True) @@ -55,7 +54,7 @@ async def handle_order_paid(order_id: str, merchant_pubkey: str): ) merchant = await get_merchant_by_pubkey(merchant_pubkey) - assert merchant, f"Merchant cannot be foud for order {order_id}" + assert merchant, f"Merchant cannot be found for order {order_id}" dm_content = json.dumps( order_status.dict(), separators=(",", ":"), ensure_ascii=False ) diff --git a/views_api.py b/views_api.py index 32696e4..29f6cd3 100644 --- a/views_api.py +++ b/views_api.py @@ -38,6 +38,7 @@ from .crud import ( get_wallet_for_product, get_zone, get_zones, + update_order_shipped_status, update_product, update_stall, update_zone, @@ -47,6 +48,7 @@ from .models import ( Nostrable, Order, OrderExtra, + OrderStatusUpdate, PartialMerchant, PartialOrder, PartialProduct, @@ -542,24 +544,35 @@ async def api_get_orders(wallet: WalletTypeInfo = Depends(get_key_type)): ) -# @nostrmarket_ext.patch("/api/v1/order/{order_id}") -# async def api_update_order( -# data: OrderStatusUpdate, -# wallet: WalletTypeInfo = Depends(require_admin_key), -# ) -> Zone: -# try: +@nostrmarket_ext.patch("/api/v1/order/{order_id}") +async def api_update_order_status( + data: OrderStatusUpdate, + wallet: WalletTypeInfo = Depends(require_admin_key), +) -> Order: + try: + assert data.shipped != None, "Shipped value is required for order" + order = await update_order_shipped_status( + wallet.wallet.user, data.id, data.shipped + ) + assert order, "Cannot find updated order" -# 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", -# ) + merchant = await get_merchant_for_user(wallet.wallet.user) + assert merchant, f"Merchant cannot be found for order {data.id}" + + data.paid = order.paid + dm_content = json.dumps(data.dict(), separators=(",", ":"), ensure_ascii=False) + + dm_event = merchant.build_dm_event(dm_content, order.pubkey) + await publish_nostr_event(dm_event) + + return order + + except Exception as ex: + logger.warning(ex) + raise HTTPException( + status_code=HTTPStatus.INTERNAL_SERVER_ERROR, + detail="Cannot update order", + ) ######################################## OTHER ######################################## From 30511b1fce674e2a944d02b12740c52064c61079 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Tue, 7 Mar 2023 14:30:58 +0200 Subject: [PATCH 166/891] feat: add ship/unship functionality --- crud.py | 8 +- static/components/order-list/order-list.html | 84 +++++++++++++------- static/components/order-list/order-list.js | 37 ++++++++- tasks.py | 3 +- views_api.py | 47 +++++++---- 5 files changed, 129 insertions(+), 50 deletions(-) diff --git a/crud.py b/crud.py index b8ee099..2e6013b 100644 --- a/crud.py +++ b/crud.py @@ -392,10 +392,12 @@ async def update_order_paid_status(order_id: str, paid: bool) -> Optional[Order] return Order.from_row(row) if row else None -async def update_order_shipped_status(order_id: str, shipped: bool) -> Optional[Order]: +async def update_order_shipped_status( + user_id: str, order_id: str, shipped: bool +) -> Optional[Order]: await db.execute( - f"UPDATE nostrmarket.orders SET shipped = ? WHERE id = ?", - (order_id, shipped), + f"UPDATE nostrmarket.orders SET shipped = ? WHERE user_id = ? AND id = ?", + (shipped, user_id, order_id), ) row = await db.fetchone( diff --git a/static/components/order-list/order-list.html b/static/components/order-list/order-list.html index a0968e7..e629b1f 100644 --- a/static/components/order-list/order-list.html +++ b/static/components/order-list/order-list.html @@ -37,6 +37,7 @@ @@ -49,6 +50,33 @@ +
+
Products:
+
+
+
Quantity
+
+
Name
+
+
+
+
+
+
+
+
+
{{item.quantity}}
+
x
+
+ {{productOverview(props.row, item.product_id)}} +
+
+
+
+
Order ID:
@@ -78,33 +106,7 @@
-
-
Products:
-
-
-
Quantity
-
-
Name
-
-
-
-
-
-
-
-
-
{{item.quantity}}
-
x
-
- {{productOverview(props.row, item.product_id)}} -
-
-
-
-
+
Customer Public Key:
@@ -172,4 +174,32 @@ + + + + + + +
+ + + Cancel +
+
+
+
diff --git a/static/components/order-list/order-list.js b/static/components/order-list/order-list.js index bb4f335..482cc8a 100644 --- a/static/components/order-list/order-list.js +++ b/static/components/order-list/order-list.js @@ -8,7 +8,9 @@ async function orderList(path) { data: function () { return { orders: [], - + selectedOrder: null, + shippingMessage: '', + showShipDialog: false, filter: '', ordersTable: { columns: [ @@ -93,6 +95,39 @@ async function orderList(path) { } catch (error) { LNbits.utils.notifyApiError(error) } + }, + updateOrderShipped: async function () { + console.log('### order', this.selectedOrder) + this.selectedOrder.shipped = !this.selectedOrder.shipped + try { + await LNbits.api.request( + 'PATCH', + `/nostrmarket/api/v1/order/${this.selectedOrder.id}`, + this.adminkey, + { + id: this.selectedOrder.id, + message: this.shippingMessage, + shipped: this.selectedOrder.shipped + } + ) + this.$q.notify({ + type: 'positive', + message: 'Order updated!' + }) + } catch (error) { + LNbits.utils.notifyApiError(error) + } + this.showShipDialog = false + }, + showShipOrderDialog: function (order) { + this.selectedOrder = order + this.shippingMessage = order.shipped + ? `The order has been shipped! Order ID: '${order.id}' ` + : `The order has NOT yet been shipped! Order ID: '${order.id}'` + + // do not change the status yet + this.selectedOrder.shipped = !order.shipped + this.showShipDialog = true } }, created: async function () { diff --git a/tasks.py b/tasks.py index f5afec3..c97c2b2 100644 --- a/tasks.py +++ b/tasks.py @@ -45,7 +45,6 @@ async def on_invoice_paid(payment: Payment) -> None: await handle_order_paid(order_id, merchant_pubkey) - async def handle_order_paid(order_id: str, merchant_pubkey: str): try: order = await update_order_paid_status(order_id, True) @@ -55,7 +54,7 @@ async def handle_order_paid(order_id: str, merchant_pubkey: str): ) merchant = await get_merchant_by_pubkey(merchant_pubkey) - assert merchant, f"Merchant cannot be foud for order {order_id}" + assert merchant, f"Merchant cannot be found for order {order_id}" dm_content = json.dumps( order_status.dict(), separators=(",", ":"), ensure_ascii=False ) diff --git a/views_api.py b/views_api.py index 32696e4..29f6cd3 100644 --- a/views_api.py +++ b/views_api.py @@ -38,6 +38,7 @@ from .crud import ( get_wallet_for_product, get_zone, get_zones, + update_order_shipped_status, update_product, update_stall, update_zone, @@ -47,6 +48,7 @@ from .models import ( Nostrable, Order, OrderExtra, + OrderStatusUpdate, PartialMerchant, PartialOrder, PartialProduct, @@ -542,24 +544,35 @@ async def api_get_orders(wallet: WalletTypeInfo = Depends(get_key_type)): ) -# @nostrmarket_ext.patch("/api/v1/order/{order_id}") -# async def api_update_order( -# data: OrderStatusUpdate, -# wallet: WalletTypeInfo = Depends(require_admin_key), -# ) -> Zone: -# try: +@nostrmarket_ext.patch("/api/v1/order/{order_id}") +async def api_update_order_status( + data: OrderStatusUpdate, + wallet: WalletTypeInfo = Depends(require_admin_key), +) -> Order: + try: + assert data.shipped != None, "Shipped value is required for order" + order = await update_order_shipped_status( + wallet.wallet.user, data.id, data.shipped + ) + assert order, "Cannot find updated order" -# 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", -# ) + merchant = await get_merchant_for_user(wallet.wallet.user) + assert merchant, f"Merchant cannot be found for order {data.id}" + + data.paid = order.paid + dm_content = json.dumps(data.dict(), separators=(",", ":"), ensure_ascii=False) + + dm_event = merchant.build_dm_event(dm_content, order.pubkey) + await publish_nostr_event(dm_event) + + return order + + except Exception as ex: + logger.warning(ex) + raise HTTPException( + status_code=HTTPStatus.INTERNAL_SERVER_ERROR, + detail="Cannot update order", + ) ######################################## OTHER ######################################## From 9558181914f6301fb899effcd2fcbe5cd462faba Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Tue, 7 Mar 2023 14:34:05 +0200 Subject: [PATCH 167/891] feat: add `destroy` extension enpoint --- views_api.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/views_api.py b/views_api.py index 29f6cd3..6d561d8 100644 --- a/views_api.py +++ b/views_api.py @@ -9,13 +9,14 @@ from loguru import logger from lnbits.core import create_invoice from lnbits.decorators import ( WalletTypeInfo, + check_admin, get_key_type, require_admin_key, require_invoice_key, ) from lnbits.utils.exchange_rates import currencies -from . import nostrmarket_ext +from . import nostrmarket_ext, scheduled_tasks from .crud import ( create_merchant, create_order, @@ -583,6 +584,16 @@ async def api_list_currencies_available(): return list(currencies.keys()) +@nostrmarket_ext.delete("/api/v1", status_code=HTTPStatus.OK) +async def api_stop(wallet: WalletTypeInfo = Depends(check_admin)): + for t in scheduled_tasks: + try: + t.cancel() + except Exception as ex: + logger.warning(ex) + + return {"success": True} + ######################################## HELPERS ######################################## From 463bfd8367698dc889aaffe60b3fee606faa998e Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Tue, 7 Mar 2023 14:34:05 +0200 Subject: [PATCH 168/891] feat: add `destroy` extension enpoint --- views_api.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/views_api.py b/views_api.py index 29f6cd3..6d561d8 100644 --- a/views_api.py +++ b/views_api.py @@ -9,13 +9,14 @@ from loguru import logger from lnbits.core import create_invoice from lnbits.decorators import ( WalletTypeInfo, + check_admin, get_key_type, require_admin_key, require_invoice_key, ) from lnbits.utils.exchange_rates import currencies -from . import nostrmarket_ext +from . import nostrmarket_ext, scheduled_tasks from .crud import ( create_merchant, create_order, @@ -583,6 +584,16 @@ async def api_list_currencies_available(): return list(currencies.keys()) +@nostrmarket_ext.delete("/api/v1", status_code=HTTPStatus.OK) +async def api_stop(wallet: WalletTypeInfo = Depends(check_admin)): + for t in scheduled_tasks: + try: + t.cancel() + except Exception as ex: + logger.warning(ex) + + return {"success": True} + ######################################## HELPERS ######################################## From 46a9f9c01ee15a77352a2981ca52a24e8b4324ed Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Tue, 7 Mar 2023 13:25:16 +0000 Subject: [PATCH 169/891] login/account functionality --- static/js/market.js | 70 ++++++++++++++++- templates/nostrmarket/market.html | 120 ++++++++++++++++++++++++++++++ 2 files changed, 187 insertions(+), 3 deletions(-) diff --git a/static/js/market.js b/static/js/market.js index a7c724e..88042ce 100644 --- a/static/js/market.js +++ b/static/js/market.js @@ -2,6 +2,7 @@ const market = async () => { Vue.component(VueQrcode.name, VueQrcode) const NostrTools = window.NostrTools + const defaultRelays = [ 'wss://relay.damus.io', 'wss://relay.snort.social', @@ -23,7 +24,8 @@ const market = async () => { customerMarket('static/components/customer-market/customer-market.html'), customerStall('static/components/customer-stall/customer-stall.html'), productDetail('static/components/product-detail/product-detail.html'), - shoppingCart('static/components/shopping-cart/shopping-cart.html') + shoppingCart('static/components/shopping-cart/shopping-cart.html'), + chatDialog('static/components/chat-dialog/chat-dialog.html') ]) new Vue({ @@ -31,7 +33,15 @@ const market = async () => { mixins: [windowMixin], data: function () { return { - drawer: false, + account: null, + accountDialog: { + show: false, + data: { + watchOnly: false, + key: null + } + }, + drawer: true, pubkeys: new Set(), relays: new Set(), events: [], @@ -72,9 +82,20 @@ const market = async () => { }, isLoading() { return this.$q.loading.isActive + }, + hasExtension() { + return window.nostr + }, + isValidKey() { + return this.accountDialog.data.key + ?.toLowerCase() + ?.match(/^[0-9a-f]{64}$/) } }, async created() { + // Check for user stored + this.account = this.$q.localStorage.getItem('diagonAlley.account') || null + // Check for stored merchants and relays on localStorage try { let merchants = this.$q.localStorage.getItem(`diagonAlley.merchants`) @@ -115,7 +136,10 @@ const market = async () => { } // Get notes from Nostr - await this.initNostr() + //await this.initNostr() + + // Get fiat rates (i think there's an LNbits endpoint for this) + //await this.getRates() this.$q.loading.hide() }, methods: { @@ -129,6 +153,46 @@ const market = async () => { }) console.log(naddr) }, + async deleteAccount() { + await LNbits.utils + .confirmDialog( + `This will delete all stored data. If you didn't backup the Key Pair (Private and Public Keys), you will lose it. Continue?` + ) + .onOk(() => { + window.localStorage.removeItem('diagonAlley.account') + this.account = null + }) + }, + async createAccount(useExtension = false) { + let nip07 + if (useExtension) { + await this.getFromExtension() + nip07 = true + } + if (this.isValidKey) { + let {key, watchOnly} = this.accountDialog.data + this.$q.localStorage.set('diagonAlley.account', { + privkey: watchOnly ? null : key, + pubkey: watchOnly ? key : NostrTools.getPublicKey(key), + useExtension: nip07 ?? false + }) + this.accountDialog.data = { + watchOnly: false, + key: null + } + this.accountDialog.show = false + this.account = this.$q.localStorage.getItem('diagonAlley.account') + } + }, + generateKeyPair() { + this.accountDialog.data.key = NostrTools.generatePrivateKey() + this.accountDialog.data.watchOnly = false + }, + async getFromExtension() { + this.accountDialog.data.key = await window.nostr.getPublicKey() + this.accountDialog.data.watchOnly = true + return + }, async initNostr() { this.$q.loading.show() const pool = new NostrTools.SimplePool() diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html index 5f01c41..77c0cac 100644 --- a/templates/nostrmarket/market.html +++ b/templates/nostrmarket/market.html @@ -5,6 +5,32 @@ Settings +
+
+ + Delete account data +
+
+ Login or Create account +
+ +
+ + + + +
Account Setup
+ + +
+ +

Type your Nostr private key or generate a new one.

+ You can also use a Nostr-capable extension. +
+ + + + + + + + + + Is this a Public Key? + + If not using an Nostr capable extension, you'll have to sign + events manually! Better to use a Private Key that you can delete + later, or just generate an ephemeral key pair to use in the + Marketplace! + + + + + + + + + + +
+
{% endblock %} {% block scripts %} @@ -165,6 +247,44 @@ + + {% endblock %} From 07dddc628b3179933357ad6a7237fc86394734d2 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Tue, 7 Mar 2023 13:25:16 +0000 Subject: [PATCH 170/891] login/account functionality --- static/js/market.js | 70 ++++++++++++++++- templates/nostrmarket/market.html | 120 ++++++++++++++++++++++++++++++ 2 files changed, 187 insertions(+), 3 deletions(-) diff --git a/static/js/market.js b/static/js/market.js index a7c724e..88042ce 100644 --- a/static/js/market.js +++ b/static/js/market.js @@ -2,6 +2,7 @@ const market = async () => { Vue.component(VueQrcode.name, VueQrcode) const NostrTools = window.NostrTools + const defaultRelays = [ 'wss://relay.damus.io', 'wss://relay.snort.social', @@ -23,7 +24,8 @@ const market = async () => { customerMarket('static/components/customer-market/customer-market.html'), customerStall('static/components/customer-stall/customer-stall.html'), productDetail('static/components/product-detail/product-detail.html'), - shoppingCart('static/components/shopping-cart/shopping-cart.html') + shoppingCart('static/components/shopping-cart/shopping-cart.html'), + chatDialog('static/components/chat-dialog/chat-dialog.html') ]) new Vue({ @@ -31,7 +33,15 @@ const market = async () => { mixins: [windowMixin], data: function () { return { - drawer: false, + account: null, + accountDialog: { + show: false, + data: { + watchOnly: false, + key: null + } + }, + drawer: true, pubkeys: new Set(), relays: new Set(), events: [], @@ -72,9 +82,20 @@ const market = async () => { }, isLoading() { return this.$q.loading.isActive + }, + hasExtension() { + return window.nostr + }, + isValidKey() { + return this.accountDialog.data.key + ?.toLowerCase() + ?.match(/^[0-9a-f]{64}$/) } }, async created() { + // Check for user stored + this.account = this.$q.localStorage.getItem('diagonAlley.account') || null + // Check for stored merchants and relays on localStorage try { let merchants = this.$q.localStorage.getItem(`diagonAlley.merchants`) @@ -115,7 +136,10 @@ const market = async () => { } // Get notes from Nostr - await this.initNostr() + //await this.initNostr() + + // Get fiat rates (i think there's an LNbits endpoint for this) + //await this.getRates() this.$q.loading.hide() }, methods: { @@ -129,6 +153,46 @@ const market = async () => { }) console.log(naddr) }, + async deleteAccount() { + await LNbits.utils + .confirmDialog( + `This will delete all stored data. If you didn't backup the Key Pair (Private and Public Keys), you will lose it. Continue?` + ) + .onOk(() => { + window.localStorage.removeItem('diagonAlley.account') + this.account = null + }) + }, + async createAccount(useExtension = false) { + let nip07 + if (useExtension) { + await this.getFromExtension() + nip07 = true + } + if (this.isValidKey) { + let {key, watchOnly} = this.accountDialog.data + this.$q.localStorage.set('diagonAlley.account', { + privkey: watchOnly ? null : key, + pubkey: watchOnly ? key : NostrTools.getPublicKey(key), + useExtension: nip07 ?? false + }) + this.accountDialog.data = { + watchOnly: false, + key: null + } + this.accountDialog.show = false + this.account = this.$q.localStorage.getItem('diagonAlley.account') + } + }, + generateKeyPair() { + this.accountDialog.data.key = NostrTools.generatePrivateKey() + this.accountDialog.data.watchOnly = false + }, + async getFromExtension() { + this.accountDialog.data.key = await window.nostr.getPublicKey() + this.accountDialog.data.watchOnly = true + return + }, async initNostr() { this.$q.loading.show() const pool = new NostrTools.SimplePool() diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html index 5f01c41..77c0cac 100644 --- a/templates/nostrmarket/market.html +++ b/templates/nostrmarket/market.html @@ -5,6 +5,32 @@ Settings +
+
+ + Delete account data +
+
+ Login or Create account +
+ +
+ + + + +
Account Setup
+ + +
+ +

Type your Nostr private key or generate a new one.

+ You can also use a Nostr-capable extension. +
+ + + + + + + + + + Is this a Public Key? + + If not using an Nostr capable extension, you'll have to sign + events manually! Better to use a Private Key that you can delete + later, or just generate an ephemeral key pair to use in the + Marketplace! + + + + + + + + + + +
+
{% endblock %} {% block scripts %} @@ -165,6 +247,44 @@ + + {% endblock %} From 5c852f11eb24ef1c1f8f32e657fe6b96b9b90669 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Wed, 8 Mar 2023 10:01:03 +0000 Subject: [PATCH 171/891] remove exchange rates --- .../customer-market/customer-market.js | 2 +- .../customer-stall/customer-stall.html | 72 +++++++-- .../customer-stall/customer-stall.js | 144 +++++++++++------- .../components/product-card/product-card.html | 3 - .../product-detail/product-detail.html | 3 - static/js/market.js | 25 +-- static/js/utils.js | 12 ++ templates/nostrmarket/market.html | 3 +- 8 files changed, 163 insertions(+), 101 deletions(-) diff --git a/static/components/customer-market/customer-market.js b/static/components/customer-market/customer-market.js index c14ffa9..45d5bea 100644 --- a/static/components/customer-market/customer-market.js +++ b/static/components/customer-market/customer-market.js @@ -4,7 +4,7 @@ async function customerMarket(path) { name: 'customer-market', template, - props: ['products', 'exchange-rates', 'change-page'], + props: ['products', 'change-page'], data: function () { return {} }, diff --git a/static/components/customer-stall/customer-stall.html b/static/components/customer-stall/customer-stall.html index c6ac229..ea63043 100644 --- a/static/components/customer-stall/customer-stall.html +++ b/static/components/customer-stall/customer-stall.html @@ -10,6 +10,12 @@ + @@ -64,12 +71,12 @@ filled dense readonly - hint="This your key pair! Don't lose it!" - v-if="checkoutDialog.data.privkey" - v-model="checkoutDialog.data.privkey" + type="password" + v-if="customerPrivkey" + v-model="customerPrivkey" > -
+
- Total: {{ stall.currency != 'sat' ? getAmountFormated(finalCost) : - finalCost + 'sats' }} - ({{ getValueInSats(finalCost) }} sats) + > -->
+ +
+ +
+
+ + +
+ Copy invoice + Close +
+
+
diff --git a/static/components/customer-stall/customer-stall.js b/static/components/customer-stall/customer-stall.js index b72ba7d..4bd74c3 100644 --- a/static/components/customer-stall/customer-stall.js +++ b/static/components/customer-stall/customer-stall.js @@ -6,15 +6,16 @@ async function customerStall(path) { template, props: [ + 'account', 'stall', 'products', - 'exchange-rates', 'product-detail', 'change-page', 'relays' ], data: function () { return { + loading: false, cart: { total: 0, size: 0, @@ -23,8 +24,9 @@ async function customerStall(path) { cartMenu: [], hasNip07: false, customerPubkey: null, - customerPrivKey: null, - nostrMessages: new Map(), + customerPrivkey: null, + customerUseExtension: null, + activeOrder: null, checkoutDialog: { show: false, data: { @@ -58,12 +60,6 @@ async function customerStall(path) { changePageS(page, opts) { this.$emit('change-page', page, opts) }, - getValueInSats(amount, unit = 'USD') { - if (!this.exchangeRates) return 0 - return Math.ceil( - (amount / this.exchangeRates[`BTC${unit}`][unit]) * 1e8 - ) - }, getAmountFormated(amount, unit = 'USD') { return LNbits.utils.formatCurrency(amount, unit) }, @@ -125,26 +121,12 @@ async function customerStall(path) { } } }, - async getPubkey() { - try { - this.customerPubkey = await window.nostr.getPublicKey() - this.checkoutDialog.data.pubkey = this.customerPubkey - this.checkoutDialog.data.privkey = null - } catch (err) { - console.error( - `Failed to get a public key from a Nostr extension: ${err}` - ) - } - }, - generateKeyPair() { - let sk = NostrTools.generatePrivateKey() - let pk = NostrTools.getPublicKey(sk) - this.customerPubkey = pk - this.customerPrivKey = sk - this.checkoutDialog.data.pubkey = this.customerPubkey - this.checkoutDialog.data.privkey = this.customerPrivKey + closeQrCodeDialog() { + this.qrCodeDialog.dismissMsg() + this.qrCodeDialog.show = false }, async placeOrder() { + this.loading = true LNbits.utils .confirmDialog( `Send the order to the merchant? You should receive a message with the payment details.` @@ -170,6 +152,7 @@ async function customerStall(path) { ':' ) ) + this.activeOrder = orderObj.id let event = { ...(await NostrTools.getBlankEvent()), kind: 4, @@ -177,13 +160,13 @@ async function customerStall(path) { tags: [['p', this.stall.pubkey]], pubkey: this.customerPubkey } - if (this.customerPrivKey) { + if (this.customerPrivkey) { event.content = await NostrTools.nip04.encrypt( - this.customerPrivKey, + this.customerPrivkey, this.stall.pubkey, JSON.stringify(orderObj) ) - } else { + } else if (this.customerUseExtension && this.hasNip07) { event.content = await window.nostr.nip04.encrypt( this.stall.pubkey, JSON.stringify(orderObj) @@ -196,15 +179,15 @@ async function customerStall(path) { } } event.id = NostrTools.getEventHash(event) - if (this.customerPrivKey) { + if (this.customerPrivkey) { event.sig = await NostrTools.signEvent( event, - this.customerPrivKey + this.customerPrivkey ) - } else if (this.hasNip07) { + } else if (this.customerUseExtension && this.hasNip07) { event = await window.nostr.signEvent(event) } - console.log(event, orderObj) + await this.sendOrder(event) }) }, @@ -223,25 +206,32 @@ async function customerStall(path) { let pub = relay.publish(order) pub.on('ok', () => { console.log(`${relay.url} has accepted our event`) + relay.close() }) pub.on('failed', reason => { console.log(`failed to publish to ${relay.url}: ${reason}`) + relay.close() }) } catch (err) { console.error(`Error: ${err}`) } } + this.loading = false this.resetCheckout() + this.resetCart() + this.qrCodeDialog.show = true + this.qrCodeDialog.dismissMsg = this.$q.notify({ + timeout: 0, + message: 'Waiting for invoice from merchant...' + }) this.listenMessages() }, async listenMessages() { + console.log('LISTEN') try { const pool = new NostrTools.SimplePool() const filters = [ - { - kinds: [4], - authors: [this.customerPubkey] - }, + // / { kinds: [4], '#p': [this.customerPubkey] @@ -254,38 +244,74 @@ async function customerStall(path) { let sender = mine ? event.tags.find(([k, v]) => k === 'p' && v && v !== '')[1] : event.pubkey - if ( - (mine && sender != this.stall.pubkey) || - (!mine && sender != this.customerPubkey) - ) { - console.log(`Not relevant message!`) - return - } + try { - let plaintext = this.customerPrivKey - ? await NostrTools.nip04.decrypt( - this.customerPrivKey, - sender, - event.content - ) - : await window.nostr.nip04.decrypt(sender, event.content) - // console.log(`${mine ? 'Me' : 'Customer'}: ${plaintext}`) - this.nostrMessages.set(event.id, { - msg: plaintext, - timestamp: event.created_at, - sender: `${mine ? 'Me' : 'Merchant'}` - }) + let plaintext + if (this.customerPrivkey) { + plaintext = await NostrTools.nip04.decrypt( + this.customerPrivkey, + sender, + event.content + ) + } else if (this.customerUseExtension && this.hasNip07) { + plaintext = await window.nostr.nip04.decrypt( + sender, + event.content + ) + } + console.log(`${mine ? 'Me' : 'Merchant'}: ${plaintext}`) + + // this.nostrMessages.set(event.id, { + // msg: plaintext, + // timestamp: event.created_at, + // sender: `${mine ? 'Me' : 'Merchant'}` + // }) + this.messageFilter(plaintext, cb => Promise.resolve(pool.close)) } catch { console.error('Unable to decrypt message!') - return } }) } catch (err) { console.error(`Error: ${err}`) } + }, + messageFilter(text, cb = () => {}) { + if (!isJson(text)) return + let json = JSON.parse(text) + if (json.id != this.activeOrder) return + if (json?.payment_options) { + // this.qrCodeDialog.show = true + this.qrCodeDialog.data.payment_request = json.payment_options.find( + o => o.type == 'ln' + ).link + this.qrCodeDialog.dismissMsg = this.$q.notify({ + timeout: 0, + message: 'Waiting for payment...' + }) + } else if (json?.paid) { + this.qrCodeDialog.dismissMsg = this.$q.notify({ + type: 'positive', + message: 'Sats received, thanks!', + icon: 'thumb_up' + }) + this.closeQrCodeDialog() + this.activeOrder = null + Promise.resolve(cb()) + } else { + return + } } + // async mockInit() { + // this.customerPubkey = await window.nostr.getPublicKey() + // this.activeOrder = + // 'e4a16aa0198022dc682b2b52ed15767438282c0e712f510332fc047eaf795313' + // await this.listenMessages() + // } }, created() { + this.customerPubkey = this.account.pubkey + this.customerPrivkey = this.account.privkey + this.customerUseExtension = this.account.useExtension setTimeout(() => { if (window.nostr) { this.hasNip07 = true diff --git a/static/components/product-card/product-card.html b/static/components/product-card/product-card.html index 5a92ba3..95f86e2 100644 --- a/static/components/product-card/product-card.html +++ b/static/components/product-card/product-card.html @@ -43,9 +43,6 @@ {{ product.formatedPrice }} - ({{ product.priceInSats }} sats) {{ product.quantity }} left {{ product.formatedPrice }} - ({{ product.priceInSats }} sats) { key: null } }, - drawer: true, + drawer: false, pubkeys: new Set(), relays: new Set(), events: [], @@ -49,7 +49,6 @@ const market = async () => { products: [], profiles: new Map(), searchText: null, - exchangeRates: null, inputPubkey: null, inputRelay: null, activePage: 'market', @@ -136,10 +135,8 @@ const market = async () => { } // Get notes from Nostr - //await this.initNostr() + await this.initNostr() - // Get fiat rates (i think there's an LNbits endpoint for this) - //await this.getRates() this.$q.loading.hide() }, methods: { @@ -233,23 +230,12 @@ const market = async () => { obj.images = [obj.image] if (obj.currency != 'sat') { obj.formatedPrice = this.getAmountFormated(obj.price, obj.currency) - obj.priceInSats = this.getValueInSats(obj.price, obj.currency) } return obj }) pool.close(relays) return }, - async getRates() { - let noFiat = this.stalls.map(s => s.currency).every(c => c == 'sat') - if (noFiat) return - try { - let rates = await axios.get('https://api.opennode.co/v1/rates') - this.exchangeRates = rates.data.data - } catch (error) { - LNbits.utils.notifyApiError(error) - } - }, navigateTo(page, opts = {stall: null, product: null, pubkey: null}) { let {stall, product, pubkey} = opts let url = new URL(window.location) @@ -283,13 +269,6 @@ const market = async () => { window.history.pushState({}, '', url) this.activePage = page }, - - getValueInSats(amount, unit = 'USD') { - if (!this.exchangeRates) return 0 - return Math.ceil( - (amount / this.exchangeRates[`BTC${unit}`][unit]) * 1e8 - ) - }, getAmountFormated(amount, unit = 'USD') { return LNbits.utils.formatCurrency(amount, unit) }, diff --git a/static/js/utils.js b/static/js/utils.js index e684ab8..c11cdef 100644 --- a/static/js/utils.js +++ b/static/js/utils.js @@ -26,3 +26,15 @@ async function hash(string) { .join('') return hashHex } + +function isJson(str) { + if (typeof str !== 'string') { + return false + } + try { + JSON.parse(str) + return true + } catch (error) { + return false + } +} diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html index 77c0cac..20a4332 100644 --- a/templates/nostrmarket/market.html +++ b/templates/nostrmarket/market.html @@ -169,15 +169,14 @@ v-if="!isLoading && activeStall" :stall="stalls.find(stall => stall.id == activeStall)" :products="filterProducts" - :exchange-rates="exchangeRates" :product-detail="activeProduct" :relays="relays" + :account="account" @change-page="navigateTo" > From 91f6a9d36cd5d555aa5d166500b1b35f842e9908 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Wed, 8 Mar 2023 10:01:03 +0000 Subject: [PATCH 172/891] remove exchange rates --- .../customer-market/customer-market.js | 2 +- .../customer-stall/customer-stall.html | 72 +++++++-- .../customer-stall/customer-stall.js | 144 +++++++++++------- .../components/product-card/product-card.html | 3 - .../product-detail/product-detail.html | 3 - static/js/market.js | 25 +-- static/js/utils.js | 12 ++ templates/nostrmarket/market.html | 3 +- 8 files changed, 163 insertions(+), 101 deletions(-) diff --git a/static/components/customer-market/customer-market.js b/static/components/customer-market/customer-market.js index c14ffa9..45d5bea 100644 --- a/static/components/customer-market/customer-market.js +++ b/static/components/customer-market/customer-market.js @@ -4,7 +4,7 @@ async function customerMarket(path) { name: 'customer-market', template, - props: ['products', 'exchange-rates', 'change-page'], + props: ['products', 'change-page'], data: function () { return {} }, diff --git a/static/components/customer-stall/customer-stall.html b/static/components/customer-stall/customer-stall.html index c6ac229..ea63043 100644 --- a/static/components/customer-stall/customer-stall.html +++ b/static/components/customer-stall/customer-stall.html @@ -10,6 +10,12 @@ + @@ -64,12 +71,12 @@ filled dense readonly - hint="This your key pair! Don't lose it!" - v-if="checkoutDialog.data.privkey" - v-model="checkoutDialog.data.privkey" + type="password" + v-if="customerPrivkey" + v-model="customerPrivkey" > -
+
- Total: {{ stall.currency != 'sat' ? getAmountFormated(finalCost) : - finalCost + 'sats' }} - ({{ getValueInSats(finalCost) }} sats) + > -->
+ +
+ +
+
+ + +
+ Copy invoice + Close +
+
+
diff --git a/static/components/customer-stall/customer-stall.js b/static/components/customer-stall/customer-stall.js index b72ba7d..4bd74c3 100644 --- a/static/components/customer-stall/customer-stall.js +++ b/static/components/customer-stall/customer-stall.js @@ -6,15 +6,16 @@ async function customerStall(path) { template, props: [ + 'account', 'stall', 'products', - 'exchange-rates', 'product-detail', 'change-page', 'relays' ], data: function () { return { + loading: false, cart: { total: 0, size: 0, @@ -23,8 +24,9 @@ async function customerStall(path) { cartMenu: [], hasNip07: false, customerPubkey: null, - customerPrivKey: null, - nostrMessages: new Map(), + customerPrivkey: null, + customerUseExtension: null, + activeOrder: null, checkoutDialog: { show: false, data: { @@ -58,12 +60,6 @@ async function customerStall(path) { changePageS(page, opts) { this.$emit('change-page', page, opts) }, - getValueInSats(amount, unit = 'USD') { - if (!this.exchangeRates) return 0 - return Math.ceil( - (amount / this.exchangeRates[`BTC${unit}`][unit]) * 1e8 - ) - }, getAmountFormated(amount, unit = 'USD') { return LNbits.utils.formatCurrency(amount, unit) }, @@ -125,26 +121,12 @@ async function customerStall(path) { } } }, - async getPubkey() { - try { - this.customerPubkey = await window.nostr.getPublicKey() - this.checkoutDialog.data.pubkey = this.customerPubkey - this.checkoutDialog.data.privkey = null - } catch (err) { - console.error( - `Failed to get a public key from a Nostr extension: ${err}` - ) - } - }, - generateKeyPair() { - let sk = NostrTools.generatePrivateKey() - let pk = NostrTools.getPublicKey(sk) - this.customerPubkey = pk - this.customerPrivKey = sk - this.checkoutDialog.data.pubkey = this.customerPubkey - this.checkoutDialog.data.privkey = this.customerPrivKey + closeQrCodeDialog() { + this.qrCodeDialog.dismissMsg() + this.qrCodeDialog.show = false }, async placeOrder() { + this.loading = true LNbits.utils .confirmDialog( `Send the order to the merchant? You should receive a message with the payment details.` @@ -170,6 +152,7 @@ async function customerStall(path) { ':' ) ) + this.activeOrder = orderObj.id let event = { ...(await NostrTools.getBlankEvent()), kind: 4, @@ -177,13 +160,13 @@ async function customerStall(path) { tags: [['p', this.stall.pubkey]], pubkey: this.customerPubkey } - if (this.customerPrivKey) { + if (this.customerPrivkey) { event.content = await NostrTools.nip04.encrypt( - this.customerPrivKey, + this.customerPrivkey, this.stall.pubkey, JSON.stringify(orderObj) ) - } else { + } else if (this.customerUseExtension && this.hasNip07) { event.content = await window.nostr.nip04.encrypt( this.stall.pubkey, JSON.stringify(orderObj) @@ -196,15 +179,15 @@ async function customerStall(path) { } } event.id = NostrTools.getEventHash(event) - if (this.customerPrivKey) { + if (this.customerPrivkey) { event.sig = await NostrTools.signEvent( event, - this.customerPrivKey + this.customerPrivkey ) - } else if (this.hasNip07) { + } else if (this.customerUseExtension && this.hasNip07) { event = await window.nostr.signEvent(event) } - console.log(event, orderObj) + await this.sendOrder(event) }) }, @@ -223,25 +206,32 @@ async function customerStall(path) { let pub = relay.publish(order) pub.on('ok', () => { console.log(`${relay.url} has accepted our event`) + relay.close() }) pub.on('failed', reason => { console.log(`failed to publish to ${relay.url}: ${reason}`) + relay.close() }) } catch (err) { console.error(`Error: ${err}`) } } + this.loading = false this.resetCheckout() + this.resetCart() + this.qrCodeDialog.show = true + this.qrCodeDialog.dismissMsg = this.$q.notify({ + timeout: 0, + message: 'Waiting for invoice from merchant...' + }) this.listenMessages() }, async listenMessages() { + console.log('LISTEN') try { const pool = new NostrTools.SimplePool() const filters = [ - { - kinds: [4], - authors: [this.customerPubkey] - }, + // / { kinds: [4], '#p': [this.customerPubkey] @@ -254,38 +244,74 @@ async function customerStall(path) { let sender = mine ? event.tags.find(([k, v]) => k === 'p' && v && v !== '')[1] : event.pubkey - if ( - (mine && sender != this.stall.pubkey) || - (!mine && sender != this.customerPubkey) - ) { - console.log(`Not relevant message!`) - return - } + try { - let plaintext = this.customerPrivKey - ? await NostrTools.nip04.decrypt( - this.customerPrivKey, - sender, - event.content - ) - : await window.nostr.nip04.decrypt(sender, event.content) - // console.log(`${mine ? 'Me' : 'Customer'}: ${plaintext}`) - this.nostrMessages.set(event.id, { - msg: plaintext, - timestamp: event.created_at, - sender: `${mine ? 'Me' : 'Merchant'}` - }) + let plaintext + if (this.customerPrivkey) { + plaintext = await NostrTools.nip04.decrypt( + this.customerPrivkey, + sender, + event.content + ) + } else if (this.customerUseExtension && this.hasNip07) { + plaintext = await window.nostr.nip04.decrypt( + sender, + event.content + ) + } + console.log(`${mine ? 'Me' : 'Merchant'}: ${plaintext}`) + + // this.nostrMessages.set(event.id, { + // msg: plaintext, + // timestamp: event.created_at, + // sender: `${mine ? 'Me' : 'Merchant'}` + // }) + this.messageFilter(plaintext, cb => Promise.resolve(pool.close)) } catch { console.error('Unable to decrypt message!') - return } }) } catch (err) { console.error(`Error: ${err}`) } + }, + messageFilter(text, cb = () => {}) { + if (!isJson(text)) return + let json = JSON.parse(text) + if (json.id != this.activeOrder) return + if (json?.payment_options) { + // this.qrCodeDialog.show = true + this.qrCodeDialog.data.payment_request = json.payment_options.find( + o => o.type == 'ln' + ).link + this.qrCodeDialog.dismissMsg = this.$q.notify({ + timeout: 0, + message: 'Waiting for payment...' + }) + } else if (json?.paid) { + this.qrCodeDialog.dismissMsg = this.$q.notify({ + type: 'positive', + message: 'Sats received, thanks!', + icon: 'thumb_up' + }) + this.closeQrCodeDialog() + this.activeOrder = null + Promise.resolve(cb()) + } else { + return + } } + // async mockInit() { + // this.customerPubkey = await window.nostr.getPublicKey() + // this.activeOrder = + // 'e4a16aa0198022dc682b2b52ed15767438282c0e712f510332fc047eaf795313' + // await this.listenMessages() + // } }, created() { + this.customerPubkey = this.account.pubkey + this.customerPrivkey = this.account.privkey + this.customerUseExtension = this.account.useExtension setTimeout(() => { if (window.nostr) { this.hasNip07 = true diff --git a/static/components/product-card/product-card.html b/static/components/product-card/product-card.html index 5a92ba3..95f86e2 100644 --- a/static/components/product-card/product-card.html +++ b/static/components/product-card/product-card.html @@ -43,9 +43,6 @@
{{ product.formatedPrice }} - ({{ product.priceInSats }} sats) {{ product.quantity }} left {{ product.formatedPrice }} - ({{ product.priceInSats }} sats) { key: null } }, - drawer: true, + drawer: false, pubkeys: new Set(), relays: new Set(), events: [], @@ -49,7 +49,6 @@ const market = async () => { products: [], profiles: new Map(), searchText: null, - exchangeRates: null, inputPubkey: null, inputRelay: null, activePage: 'market', @@ -136,10 +135,8 @@ const market = async () => { } // Get notes from Nostr - //await this.initNostr() + await this.initNostr() - // Get fiat rates (i think there's an LNbits endpoint for this) - //await this.getRates() this.$q.loading.hide() }, methods: { @@ -233,23 +230,12 @@ const market = async () => { obj.images = [obj.image] if (obj.currency != 'sat') { obj.formatedPrice = this.getAmountFormated(obj.price, obj.currency) - obj.priceInSats = this.getValueInSats(obj.price, obj.currency) } return obj }) pool.close(relays) return }, - async getRates() { - let noFiat = this.stalls.map(s => s.currency).every(c => c == 'sat') - if (noFiat) return - try { - let rates = await axios.get('https://api.opennode.co/v1/rates') - this.exchangeRates = rates.data.data - } catch (error) { - LNbits.utils.notifyApiError(error) - } - }, navigateTo(page, opts = {stall: null, product: null, pubkey: null}) { let {stall, product, pubkey} = opts let url = new URL(window.location) @@ -283,13 +269,6 @@ const market = async () => { window.history.pushState({}, '', url) this.activePage = page }, - - getValueInSats(amount, unit = 'USD') { - if (!this.exchangeRates) return 0 - return Math.ceil( - (amount / this.exchangeRates[`BTC${unit}`][unit]) * 1e8 - ) - }, getAmountFormated(amount, unit = 'USD') { return LNbits.utils.formatCurrency(amount, unit) }, diff --git a/static/js/utils.js b/static/js/utils.js index e684ab8..c11cdef 100644 --- a/static/js/utils.js +++ b/static/js/utils.js @@ -26,3 +26,15 @@ async function hash(string) { .join('') return hashHex } + +function isJson(str) { + if (typeof str !== 'string') { + return false + } + try { + JSON.parse(str) + return true + } catch (error) { + return false + } +} diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html index 77c0cac..20a4332 100644 --- a/templates/nostrmarket/market.html +++ b/templates/nostrmarket/market.html @@ -169,15 +169,14 @@ v-if="!isLoading && activeStall" :stall="stalls.find(stall => stall.id == activeStall)" :products="filterProducts" - :exchange-rates="exchangeRates" :product-detail="activeProduct" :relays="relays" + :account="account" @change-page="navigateTo" > From 225ff162ab91224925a3ba2fe41ef16018312fc2 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Wed, 8 Mar 2023 10:01:15 +0000 Subject: [PATCH 173/891] chat dialog component --- .../components/chat-dialog/chat-dialog.html | 89 +++++++++++ static/components/chat-dialog/chat-dialog.js | 143 ++++++++++++++++++ 2 files changed, 232 insertions(+) create mode 100644 static/components/chat-dialog/chat-dialog.html create mode 100644 static/components/chat-dialog/chat-dialog.js diff --git a/static/components/chat-dialog/chat-dialog.html b/static/components/chat-dialog/chat-dialog.html new file mode 100644 index 0000000..9ab5296 --- /dev/null +++ b/static/components/chat-dialog/chat-dialog.html @@ -0,0 +1,89 @@ +
+ + + + + +
Chat Box
+ + + + Minimize + + + Maximize + + + Close + +
+ + +
+
+ +
+ +
+
+ + + + + + + +
+
+
+
+
diff --git a/static/components/chat-dialog/chat-dialog.js b/static/components/chat-dialog/chat-dialog.js new file mode 100644 index 0000000..96f82f6 --- /dev/null +++ b/static/components/chat-dialog/chat-dialog.js @@ -0,0 +1,143 @@ +async function chatDialog(path) { + const template = await loadTemplateAsync(path) + + Vue.component('chat-dialog', { + name: 'chat-dialog', + template, + + props: ['account', 'merchant', 'relays'], + data: function () { + return { + dialog: false, + maximizedToggle: true, + pool: null, + nostrMessages: [], + newMessage: '' + } + }, + computed: { + sortedMessages() { + return this.nostrMessages.sort((a, b) => a.timestamp - b.timestamp) + } + }, + methods: { + async startPool() { + let sub = this.pool.sub(Array.from(this.relays), [ + { + kinds: [4], + authors: [this.account.pubkey] + }, + { + kinds: [4], + '#p': [this.account.pubkey] + } + ]) + sub.on('event', async event => { + let mine = event.pubkey == this.account.pubkey + let sender = mine + ? event.tags.find(([k, v]) => k === 'p' && v && v !== '')[1] + : event.pubkey + + try { + let plaintext + if (this.account.privkey) { + plaintext = await NostrTools.nip04.decrypt( + this.account.privkey, + sender, + event.content + ) + } else if (this.account.useExtension && this.hasNip07) { + plaintext = await window.nostr.nip04.decrypt( + sender, + event.content + ) + } + this.nostrMessages.push({ + id: event.id, + msg: plaintext, + timestamp: event.created_at, + sender: `${mine ? 'Me' : 'Merchant'}` + }) + } catch { + console.error('Unable to decrypt message!') + } + }) + }, + async sendMessage() { + if (this.newMessage && this.newMessage.length < 1) return + let event = { + ...(await NostrTools.getBlankEvent()), + kind: 4, + created_at: Math.floor(Date.now() / 1000), + tags: [['p', this.merchant]], + pubkey: this.account.pubkey, + content: await this.encryptMsg() + } + event.id = NostrTools.getEventHash(event) + event.sig = this.signEvent(event) + for (const url of Array.from(this.relays)) { + try { + let relay = NostrTools.relayInit(url) + relay.on('connect', () => { + console.debug(`connected to ${relay.url}`) + }) + relay.on('error', () => { + console.debug(`failed to connect to ${relay.url}`) + }) + + await relay.connect() + let pub = relay.publish(event) + pub.on('ok', () => { + console.debug(`${relay.url} has accepted our event`) + relay.close() + }) + pub.on('failed', reason => { + console.debug(`failed to publish to ${relay.url}: ${reason}`) + relay.close() + }) + this.newMessage = '' + } catch (e) { + console.error(e) + } + } + }, + async encryptMsg() { + try { + let cypher + if (this.account.privkey) { + cypher = await NostrTools.nip04.encrypt( + this.account.privkey, + this.merchant, + this.newMessage + ) + } else if (this.account.useExtension && this.hasNip07) { + cypher = await window.nostr.nip04.encrypt( + this.merchant, + this.newMessage + ) + } + return cypher + } catch (e) { + console.error(e) + } + }, + async signEvent(event) { + if (this.account.privkey) { + event.sig = await NostrTools.signEvent(event, this.account.privkey) + } else if (this.account.useExtension && this.hasNip07) { + event = await window.nostr.signEvent(event) + } + return event + } + }, + created() { + this.pool = new NostrTools.SimplePool() + setTimeout(() => { + if (window.nostr) { + this.hasNip07 = true + } + }, 1000) + this.startPool() + } + }) +} From 9d5ed55c371d24278c314c4a9f8a8f1a50b9ff80 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Wed, 8 Mar 2023 10:01:15 +0000 Subject: [PATCH 174/891] chat dialog component --- .../components/chat-dialog/chat-dialog.html | 89 +++++++++++ static/components/chat-dialog/chat-dialog.js | 143 ++++++++++++++++++ 2 files changed, 232 insertions(+) create mode 100644 static/components/chat-dialog/chat-dialog.html create mode 100644 static/components/chat-dialog/chat-dialog.js diff --git a/static/components/chat-dialog/chat-dialog.html b/static/components/chat-dialog/chat-dialog.html new file mode 100644 index 0000000..9ab5296 --- /dev/null +++ b/static/components/chat-dialog/chat-dialog.html @@ -0,0 +1,89 @@ +
+ + + + + +
Chat Box
+ + + + Minimize + + + Maximize + + + Close + +
+ + +
+
+ +
+ +
+
+ + + + + + + +
+
+
+
+
diff --git a/static/components/chat-dialog/chat-dialog.js b/static/components/chat-dialog/chat-dialog.js new file mode 100644 index 0000000..96f82f6 --- /dev/null +++ b/static/components/chat-dialog/chat-dialog.js @@ -0,0 +1,143 @@ +async function chatDialog(path) { + const template = await loadTemplateAsync(path) + + Vue.component('chat-dialog', { + name: 'chat-dialog', + template, + + props: ['account', 'merchant', 'relays'], + data: function () { + return { + dialog: false, + maximizedToggle: true, + pool: null, + nostrMessages: [], + newMessage: '' + } + }, + computed: { + sortedMessages() { + return this.nostrMessages.sort((a, b) => a.timestamp - b.timestamp) + } + }, + methods: { + async startPool() { + let sub = this.pool.sub(Array.from(this.relays), [ + { + kinds: [4], + authors: [this.account.pubkey] + }, + { + kinds: [4], + '#p': [this.account.pubkey] + } + ]) + sub.on('event', async event => { + let mine = event.pubkey == this.account.pubkey + let sender = mine + ? event.tags.find(([k, v]) => k === 'p' && v && v !== '')[1] + : event.pubkey + + try { + let plaintext + if (this.account.privkey) { + plaintext = await NostrTools.nip04.decrypt( + this.account.privkey, + sender, + event.content + ) + } else if (this.account.useExtension && this.hasNip07) { + plaintext = await window.nostr.nip04.decrypt( + sender, + event.content + ) + } + this.nostrMessages.push({ + id: event.id, + msg: plaintext, + timestamp: event.created_at, + sender: `${mine ? 'Me' : 'Merchant'}` + }) + } catch { + console.error('Unable to decrypt message!') + } + }) + }, + async sendMessage() { + if (this.newMessage && this.newMessage.length < 1) return + let event = { + ...(await NostrTools.getBlankEvent()), + kind: 4, + created_at: Math.floor(Date.now() / 1000), + tags: [['p', this.merchant]], + pubkey: this.account.pubkey, + content: await this.encryptMsg() + } + event.id = NostrTools.getEventHash(event) + event.sig = this.signEvent(event) + for (const url of Array.from(this.relays)) { + try { + let relay = NostrTools.relayInit(url) + relay.on('connect', () => { + console.debug(`connected to ${relay.url}`) + }) + relay.on('error', () => { + console.debug(`failed to connect to ${relay.url}`) + }) + + await relay.connect() + let pub = relay.publish(event) + pub.on('ok', () => { + console.debug(`${relay.url} has accepted our event`) + relay.close() + }) + pub.on('failed', reason => { + console.debug(`failed to publish to ${relay.url}: ${reason}`) + relay.close() + }) + this.newMessage = '' + } catch (e) { + console.error(e) + } + } + }, + async encryptMsg() { + try { + let cypher + if (this.account.privkey) { + cypher = await NostrTools.nip04.encrypt( + this.account.privkey, + this.merchant, + this.newMessage + ) + } else if (this.account.useExtension && this.hasNip07) { + cypher = await window.nostr.nip04.encrypt( + this.merchant, + this.newMessage + ) + } + return cypher + } catch (e) { + console.error(e) + } + }, + async signEvent(event) { + if (this.account.privkey) { + event.sig = await NostrTools.signEvent(event, this.account.privkey) + } else if (this.account.useExtension && this.hasNip07) { + event = await window.nostr.signEvent(event) + } + return event + } + }, + created() { + this.pool = new NostrTools.SimplePool() + setTimeout(() => { + if (window.nostr) { + this.hasNip07 = true + } + }, 1000) + this.startPool() + } + }) +} From 9926cc857b63806fca4ff4a198dcf628195cb89c Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Wed, 8 Mar 2023 11:23:51 +0000 Subject: [PATCH 175/891] clean up --- static/components/customer-stall/customer-stall.html | 3 --- 1 file changed, 3 deletions(-) diff --git a/static/components/customer-stall/customer-stall.html b/static/components/customer-stall/customer-stall.html index ea63043..5e2d085 100644 --- a/static/components/customer-stall/customer-stall.html +++ b/static/components/customer-stall/customer-stall.html @@ -121,9 +121,6 @@
Total: {{ stall.currency != 'sat' ? getAmountFormated(finalCost, stall.currency) : finalCost + 'sats' }} -
Date: Wed, 8 Mar 2023 11:23:51 +0000 Subject: [PATCH 176/891] clean up --- static/components/customer-stall/customer-stall.html | 3 --- 1 file changed, 3 deletions(-) diff --git a/static/components/customer-stall/customer-stall.html b/static/components/customer-stall/customer-stall.html index ea63043..5e2d085 100644 --- a/static/components/customer-stall/customer-stall.html +++ b/static/components/customer-stall/customer-stall.html @@ -121,9 +121,6 @@
Total: {{ stall.currency != 'sat' ? getAmountFormated(finalCost, stall.currency) : finalCost + 'sats' }} -
Date: Wed, 8 Mar 2023 11:24:11 +0000 Subject: [PATCH 177/891] fix add merchant pubkey --- static/js/market.js | 6 +++++- templates/nostrmarket/market.html | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/static/js/market.js b/static/js/market.js index 0077344..0d823e9 100644 --- a/static/js/market.js +++ b/static/js/market.js @@ -222,6 +222,7 @@ const market = async () => { }) }) await Promise.resolve(sub) + this.$q.loading.hide() this.stalls = await Array.from(stalls.values()) this.products = Array.from(products.values()).map(obj => { @@ -272,7 +273,8 @@ const market = async () => { getAmountFormated(amount, unit = 'USD') { return LNbits.utils.formatCurrency(amount, unit) }, - async addPubkey(pubkey = null) { + async addPubkey(pubkey) { + console.log(pubkey, this.inputPubkey) if (!pubkey) { pubkey = String(this.inputPubkey).trim() } @@ -285,6 +287,7 @@ const market = async () => { pubkey = data.pubkey givenRelays = data.relays } + console.log(pubkey) this.pubkeys.add(pubkey) this.inputPubkey = null } catch (err) { @@ -310,6 +313,7 @@ const market = async () => { `diagonAlley.merchants`, Array.from(this.pubkeys) ) + Promise.resolve(this.initNostr()) }, async addRelay() { let relay = String(this.inputRelay).trim() diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html index 20a4332..a5ebeb3 100644 --- a/templates/nostrmarket/market.html +++ b/templates/nostrmarket/market.html @@ -44,12 +44,12 @@ - + From 0312aed50f7499a54e3154f7e0c1c70508dabd50 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Wed, 8 Mar 2023 11:24:11 +0000 Subject: [PATCH 178/891] fix add merchant pubkey --- static/js/market.js | 6 +++++- templates/nostrmarket/market.html | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/static/js/market.js b/static/js/market.js index 0077344..0d823e9 100644 --- a/static/js/market.js +++ b/static/js/market.js @@ -222,6 +222,7 @@ const market = async () => { }) }) await Promise.resolve(sub) + this.$q.loading.hide() this.stalls = await Array.from(stalls.values()) this.products = Array.from(products.values()).map(obj => { @@ -272,7 +273,8 @@ const market = async () => { getAmountFormated(amount, unit = 'USD') { return LNbits.utils.formatCurrency(amount, unit) }, - async addPubkey(pubkey = null) { + async addPubkey(pubkey) { + console.log(pubkey, this.inputPubkey) if (!pubkey) { pubkey = String(this.inputPubkey).trim() } @@ -285,6 +287,7 @@ const market = async () => { pubkey = data.pubkey givenRelays = data.relays } + console.log(pubkey) this.pubkeys.add(pubkey) this.inputPubkey = null } catch (err) { @@ -310,6 +313,7 @@ const market = async () => { `diagonAlley.merchants`, Array.from(this.pubkeys) ) + Promise.resolve(this.initNostr()) }, async addRelay() { let relay = String(this.inputRelay).trim() diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html index 20a4332..a5ebeb3 100644 --- a/templates/nostrmarket/market.html +++ b/templates/nostrmarket/market.html @@ -44,12 +44,12 @@ - + From c39eb2246dec5d7a62c7193de178efb0643131f2 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Wed, 8 Mar 2023 15:56:39 +0000 Subject: [PATCH 179/891] better-ish chat box --- .../components/chat-dialog/chat-dialog.html | 118 ++++++++---------- static/components/chat-dialog/chat-dialog.js | 81 +++++++++++- 2 files changed, 124 insertions(+), 75 deletions(-) diff --git a/static/components/chat-dialog/chat-dialog.html b/static/components/chat-dialog/chat-dialog.html index 9ab5296..fd87273 100644 --- a/static/components/chat-dialog/chat-dialog.html +++ b/static/components/chat-dialog/chat-dialog.html @@ -1,9 +1,9 @@ -
- +
+ @@ -13,77 +13,57 @@
Chat Box
- - Minimize - - - Maximize - - + + Close - -
-
- -
- -
-
- - - - - - - -
+ + + + + + + + + + + +
diff --git a/static/components/chat-dialog/chat-dialog.js b/static/components/chat-dialog/chat-dialog.js index 96f82f6..e89e26d 100644 --- a/static/components/chat-dialog/chat-dialog.js +++ b/static/components/chat-dialog/chat-dialog.js @@ -9,7 +9,7 @@ async function chatDialog(path) { data: function () { return { dialog: false, - maximizedToggle: true, + loading: false, pool: null, nostrMessages: [], newMessage: '' @@ -17,11 +17,22 @@ async function chatDialog(path) { }, computed: { sortedMessages() { - return this.nostrMessages.sort((a, b) => a.timestamp - b.timestamp) + return this.nostrMessages.sort((a, b) => b.timestamp - a.timestamp) } }, methods: { + async startDialog() { + this.dialog = true + await this.startPool() + }, + async closeDialog() { + this.dialog = false + await this.pool.close(Array.from(this.relays)) + }, async startPool() { + this.loading = true + this.pool = new NostrTools.SimplePool() + let messagesMap = new Map() let sub = this.pool.sub(Array.from(this.relays), [ { kinds: [4], @@ -32,6 +43,10 @@ async function chatDialog(path) { '#p': [this.account.pubkey] } ]) + sub.on('eose', () => { + this.loading = false + this.nostrMessages = Array.from(messagesMap.values()) + }) sub.on('event', async event => { let mine = event.pubkey == this.account.pubkey let sender = mine @@ -52,8 +67,7 @@ async function chatDialog(path) { event.content ) } - this.nostrMessages.push({ - id: event.id, + messagesMap.set(event.id, { msg: plaintext, timestamp: event.created_at, sender: `${mine ? 'Me' : 'Merchant'}` @@ -62,6 +76,10 @@ async function chatDialog(path) { console.error('Unable to decrypt message!') } }) + setTimeout(() => { + this.nostrMessages = Array.from(messagesMap.values()) + this.loading = false + }, 5000) }, async sendMessage() { if (this.newMessage && this.newMessage.length < 1) return @@ -128,16 +146,67 @@ async function chatDialog(path) { event = await window.nostr.signEvent(event) } return event + }, + timeFromNow(time) { + // Get timestamps + let unixTime = new Date(time).getTime() + if (!unixTime) return + let now = new Date().getTime() + + // Calculate difference + let difference = unixTime / 1000 - now / 1000 + + // Setup return object + let tfn = {} + + // Check if time is in the past, present, or future + tfn.when = 'now' + if (difference > 0) { + tfn.when = 'future' + } else if (difference < -1) { + tfn.when = 'past' + } + + // Convert difference to absolute + difference = Math.abs(difference) + + // Calculate time unit + if (difference / (60 * 60 * 24 * 365) > 1) { + // Years + tfn.unitOfTime = 'years' + tfn.time = Math.floor(difference / (60 * 60 * 24 * 365)) + } else if (difference / (60 * 60 * 24 * 45) > 1) { + // Months + tfn.unitOfTime = 'months' + tfn.time = Math.floor(difference / (60 * 60 * 24 * 45)) + } else if (difference / (60 * 60 * 24) > 1) { + // Days + tfn.unitOfTime = 'days' + tfn.time = Math.floor(difference / (60 * 60 * 24)) + } else if (difference / (60 * 60) > 1) { + // Hours + tfn.unitOfTime = 'hours' + tfn.time = Math.floor(difference / (60 * 60)) + } else if (difference / 60 > 1) { + // Minutes + tfn.unitOfTime = 'minutes' + tfn.time = Math.floor(difference / 60) + } else { + // Seconds + tfn.unitOfTime = 'seconds' + tfn.time = Math.floor(difference) + } + + // Return time from now data + return `${tfn.time} ${tfn.unitOfTime}` } }, created() { - this.pool = new NostrTools.SimplePool() setTimeout(() => { if (window.nostr) { this.hasNip07 = true } }, 1000) - this.startPool() } }) } From 6a94f6c3663fb5c99675900529b4734647989806 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Wed, 8 Mar 2023 15:56:39 +0000 Subject: [PATCH 180/891] better-ish chat box --- .../components/chat-dialog/chat-dialog.html | 118 ++++++++---------- static/components/chat-dialog/chat-dialog.js | 81 +++++++++++- 2 files changed, 124 insertions(+), 75 deletions(-) diff --git a/static/components/chat-dialog/chat-dialog.html b/static/components/chat-dialog/chat-dialog.html index 9ab5296..fd87273 100644 --- a/static/components/chat-dialog/chat-dialog.html +++ b/static/components/chat-dialog/chat-dialog.html @@ -1,9 +1,9 @@ -
- +
+ @@ -13,77 +13,57 @@
Chat Box
- - Minimize - - - Maximize - - + + Close - -
-
- -
- -
-
- - - - - - - -
+ + + + + + + + + + + +
diff --git a/static/components/chat-dialog/chat-dialog.js b/static/components/chat-dialog/chat-dialog.js index 96f82f6..e89e26d 100644 --- a/static/components/chat-dialog/chat-dialog.js +++ b/static/components/chat-dialog/chat-dialog.js @@ -9,7 +9,7 @@ async function chatDialog(path) { data: function () { return { dialog: false, - maximizedToggle: true, + loading: false, pool: null, nostrMessages: [], newMessage: '' @@ -17,11 +17,22 @@ async function chatDialog(path) { }, computed: { sortedMessages() { - return this.nostrMessages.sort((a, b) => a.timestamp - b.timestamp) + return this.nostrMessages.sort((a, b) => b.timestamp - a.timestamp) } }, methods: { + async startDialog() { + this.dialog = true + await this.startPool() + }, + async closeDialog() { + this.dialog = false + await this.pool.close(Array.from(this.relays)) + }, async startPool() { + this.loading = true + this.pool = new NostrTools.SimplePool() + let messagesMap = new Map() let sub = this.pool.sub(Array.from(this.relays), [ { kinds: [4], @@ -32,6 +43,10 @@ async function chatDialog(path) { '#p': [this.account.pubkey] } ]) + sub.on('eose', () => { + this.loading = false + this.nostrMessages = Array.from(messagesMap.values()) + }) sub.on('event', async event => { let mine = event.pubkey == this.account.pubkey let sender = mine @@ -52,8 +67,7 @@ async function chatDialog(path) { event.content ) } - this.nostrMessages.push({ - id: event.id, + messagesMap.set(event.id, { msg: plaintext, timestamp: event.created_at, sender: `${mine ? 'Me' : 'Merchant'}` @@ -62,6 +76,10 @@ async function chatDialog(path) { console.error('Unable to decrypt message!') } }) + setTimeout(() => { + this.nostrMessages = Array.from(messagesMap.values()) + this.loading = false + }, 5000) }, async sendMessage() { if (this.newMessage && this.newMessage.length < 1) return @@ -128,16 +146,67 @@ async function chatDialog(path) { event = await window.nostr.signEvent(event) } return event + }, + timeFromNow(time) { + // Get timestamps + let unixTime = new Date(time).getTime() + if (!unixTime) return + let now = new Date().getTime() + + // Calculate difference + let difference = unixTime / 1000 - now / 1000 + + // Setup return object + let tfn = {} + + // Check if time is in the past, present, or future + tfn.when = 'now' + if (difference > 0) { + tfn.when = 'future' + } else if (difference < -1) { + tfn.when = 'past' + } + + // Convert difference to absolute + difference = Math.abs(difference) + + // Calculate time unit + if (difference / (60 * 60 * 24 * 365) > 1) { + // Years + tfn.unitOfTime = 'years' + tfn.time = Math.floor(difference / (60 * 60 * 24 * 365)) + } else if (difference / (60 * 60 * 24 * 45) > 1) { + // Months + tfn.unitOfTime = 'months' + tfn.time = Math.floor(difference / (60 * 60 * 24 * 45)) + } else if (difference / (60 * 60 * 24) > 1) { + // Days + tfn.unitOfTime = 'days' + tfn.time = Math.floor(difference / (60 * 60 * 24)) + } else if (difference / (60 * 60) > 1) { + // Hours + tfn.unitOfTime = 'hours' + tfn.time = Math.floor(difference / (60 * 60)) + } else if (difference / 60 > 1) { + // Minutes + tfn.unitOfTime = 'minutes' + tfn.time = Math.floor(difference / 60) + } else { + // Seconds + tfn.unitOfTime = 'seconds' + tfn.time = Math.floor(difference) + } + + // Return time from now data + return `${tfn.time} ${tfn.unitOfTime}` } }, created() { - this.pool = new NostrTools.SimplePool() setTimeout(() => { if (window.nostr) { this.hasNip07 = true } }, 1000) - this.startPool() } }) } From c10a9bfaf9c0e7ea70433dcc907e325c3dbecef4 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Wed, 8 Mar 2023 15:57:16 +0000 Subject: [PATCH 181/891] payment dialog flow --- .../customer-stall/customer-stall.html | 16 +++++++--------- .../components/customer-stall/customer-stall.js | 17 ++--------------- 2 files changed, 9 insertions(+), 24 deletions(-) diff --git a/static/components/customer-stall/customer-stall.html b/static/components/customer-stall/customer-stall.html index 5e2d085..1e06b29 100644 --- a/static/components/customer-stall/customer-stall.html +++ b/static/components/customer-stall/customer-stall.html @@ -7,7 +7,10 @@ @click="$emit('change-page', 'market')" style="cursor: pointer" > - + - -
- -
-
+ + +
diff --git a/static/components/customer-stall/customer-stall.js b/static/components/customer-stall/customer-stall.js index 4bd74c3..58ab910 100644 --- a/static/components/customer-stall/customer-stall.js +++ b/static/components/customer-stall/customer-stall.js @@ -231,7 +231,6 @@ async function customerStall(path) { try { const pool = new NostrTools.SimplePool() const filters = [ - // / { kinds: [4], '#p': [this.customerPubkey] @@ -261,11 +260,6 @@ async function customerStall(path) { } console.log(`${mine ? 'Me' : 'Merchant'}: ${plaintext}`) - // this.nostrMessages.set(event.id, { - // msg: plaintext, - // timestamp: event.created_at, - // sender: `${mine ? 'Me' : 'Merchant'}` - // }) this.messageFilter(plaintext, cb => Promise.resolve(pool.close)) } catch { console.error('Unable to decrypt message!') @@ -280,7 +274,6 @@ async function customerStall(path) { let json = JSON.parse(text) if (json.id != this.activeOrder) return if (json?.payment_options) { - // this.qrCodeDialog.show = true this.qrCodeDialog.data.payment_request = json.payment_options.find( o => o.type == 'ln' ).link @@ -289,24 +282,18 @@ async function customerStall(path) { message: 'Waiting for payment...' }) } else if (json?.paid) { - this.qrCodeDialog.dismissMsg = this.$q.notify({ + this.closeQrCodeDialog() + this.$q.notify({ type: 'positive', message: 'Sats received, thanks!', icon: 'thumb_up' }) - this.closeQrCodeDialog() this.activeOrder = null Promise.resolve(cb()) } else { return } } - // async mockInit() { - // this.customerPubkey = await window.nostr.getPublicKey() - // this.activeOrder = - // 'e4a16aa0198022dc682b2b52ed15767438282c0e712f510332fc047eaf795313' - // await this.listenMessages() - // } }, created() { this.customerPubkey = this.account.pubkey From 288111a14426c1011df33e75afb88220f002eedf Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Wed, 8 Mar 2023 15:57:16 +0000 Subject: [PATCH 182/891] payment dialog flow --- .../customer-stall/customer-stall.html | 16 +++++++--------- .../components/customer-stall/customer-stall.js | 17 ++--------------- 2 files changed, 9 insertions(+), 24 deletions(-) diff --git a/static/components/customer-stall/customer-stall.html b/static/components/customer-stall/customer-stall.html index 5e2d085..1e06b29 100644 --- a/static/components/customer-stall/customer-stall.html +++ b/static/components/customer-stall/customer-stall.html @@ -7,7 +7,10 @@ @click="$emit('change-page', 'market')" style="cursor: pointer" > - + - -
- -
-
+ + +
diff --git a/static/components/customer-stall/customer-stall.js b/static/components/customer-stall/customer-stall.js index 4bd74c3..58ab910 100644 --- a/static/components/customer-stall/customer-stall.js +++ b/static/components/customer-stall/customer-stall.js @@ -231,7 +231,6 @@ async function customerStall(path) { try { const pool = new NostrTools.SimplePool() const filters = [ - // / { kinds: [4], '#p': [this.customerPubkey] @@ -261,11 +260,6 @@ async function customerStall(path) { } console.log(`${mine ? 'Me' : 'Merchant'}: ${plaintext}`) - // this.nostrMessages.set(event.id, { - // msg: plaintext, - // timestamp: event.created_at, - // sender: `${mine ? 'Me' : 'Merchant'}` - // }) this.messageFilter(plaintext, cb => Promise.resolve(pool.close)) } catch { console.error('Unable to decrypt message!') @@ -280,7 +274,6 @@ async function customerStall(path) { let json = JSON.parse(text) if (json.id != this.activeOrder) return if (json?.payment_options) { - // this.qrCodeDialog.show = true this.qrCodeDialog.data.payment_request = json.payment_options.find( o => o.type == 'ln' ).link @@ -289,24 +282,18 @@ async function customerStall(path) { message: 'Waiting for payment...' }) } else if (json?.paid) { - this.qrCodeDialog.dismissMsg = this.$q.notify({ + this.closeQrCodeDialog() + this.$q.notify({ type: 'positive', message: 'Sats received, thanks!', icon: 'thumb_up' }) - this.closeQrCodeDialog() this.activeOrder = null Promise.resolve(cb()) } else { return } } - // async mockInit() { - // this.customerPubkey = await window.nostr.getPublicKey() - // this.activeOrder = - // 'e4a16aa0198022dc682b2b52ed15767438282c0e712f510332fc047eaf795313' - // await this.listenMessages() - // } }, created() { this.customerPubkey = this.account.pubkey From 8e5edd6e2b0a9a6b702dec2743534a020e2931e6 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Wed, 8 Mar 2023 15:57:34 +0000 Subject: [PATCH 183/891] tweaks --- static/js/market.js | 2 +- templates/nostrmarket/market.html | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/static/js/market.js b/static/js/market.js index 0d823e9..3cf9b90 100644 --- a/static/js/market.js +++ b/static/js/market.js @@ -222,7 +222,6 @@ const market = async () => { }) }) await Promise.resolve(sub) - this.$q.loading.hide() this.stalls = await Array.from(stalls.values()) this.products = Array.from(products.values()).map(obj => { @@ -234,6 +233,7 @@ const market = async () => { } return obj }) + this.$q.loading.hide() pool.close(relays) return }, diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html index a5ebeb3..baa9715 100644 --- a/templates/nostrmarket/market.html +++ b/templates/nostrmarket/market.html @@ -57,7 +57,7 @@ {{ profiles.get(pub).name }} Date: Wed, 8 Mar 2023 15:57:34 +0000 Subject: [PATCH 184/891] tweaks --- static/js/market.js | 2 +- templates/nostrmarket/market.html | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/static/js/market.js b/static/js/market.js index 0d823e9..3cf9b90 100644 --- a/static/js/market.js +++ b/static/js/market.js @@ -222,7 +222,6 @@ const market = async () => { }) }) await Promise.resolve(sub) - this.$q.loading.hide() this.stalls = await Array.from(stalls.values()) this.products = Array.from(products.values()).map(obj => { @@ -234,6 +233,7 @@ const market = async () => { } return obj }) + this.$q.loading.hide() pool.close(relays) return }, diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html index a5ebeb3..baa9715 100644 --- a/templates/nostrmarket/market.html +++ b/templates/nostrmarket/market.html @@ -57,7 +57,7 @@ {{ profiles.get(pub).name }} Date: Wed, 8 Mar 2023 17:24:52 +0100 Subject: [PATCH 185/891] add license --- LICENSE | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..678845a --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 LNbits + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. From 486b9779a526541b0783ba424cb31611c70c6023 Mon Sep 17 00:00:00 2001 From: Pavol Rusnak Date: Wed, 8 Mar 2023 17:24:52 +0100 Subject: [PATCH 186/891] add license --- LICENSE | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..678845a --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 LNbits + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. From e18a2ee7eb61d36f7db649133d17e60709bc4285 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Wed, 8 Mar 2023 21:05:53 +0000 Subject: [PATCH 187/891] brush up checkout dialog --- .../components/chat-dialog/chat-dialog.html | 1 - .../customer-stall/customer-stall.html | 94 +++++++++++++++---- .../customer-stall/customer-stall.js | 25 ++++- 3 files changed, 96 insertions(+), 24 deletions(-) diff --git a/static/components/chat-dialog/chat-dialog.html b/static/components/chat-dialog/chat-dialog.html index fd87273..1e46a4d 100644 --- a/static/components/chat-dialog/chat-dialog.html +++ b/static/components/chat-dialog/chat-dialog.html @@ -2,7 +2,6 @@
@@ -62,35 +62,77 @@ v-model.trim="checkoutDialog.data.username" label="Name *optional" > + + + + It seems you haven't logged in. You can: +
    +
  1. + enter your public and private keys bellow (to sign the order + message) +
  2. +
  3. use a Nostr Signer Extension (NIP07)
  4. +
  5. + fill out the required fields, without keys, and download the + order and send as a direct message to the merchant on any + Nostr client +
  6. +
+
+ + Use a Nostr browser extension + Download the order and send manually + +
+
+ -
Download Order + Checkout @@ -154,7 +208,7 @@ position="top" @hide="closeQrCodeDialog" > - +
diff --git a/static/components/customer-stall/customer-stall.js b/static/components/customer-stall/customer-stall.js index 58ab910..b417c91 100644 --- a/static/components/customer-stall/customer-stall.js +++ b/static/components/customer-stall/customer-stall.js @@ -16,6 +16,7 @@ async function customerStall(path) { data: function () { return { loading: false, + isPwd: true, cart: { total: 0, size: 0, @@ -113,6 +114,24 @@ async function customerStall(path) { products: new Map() } }, + async downloadOrder() { + return + }, + async getFromExtension() { + this.customerPubkey = await window.nostr.getPublicKey() + this.customerUseExtension = true + this.checkoutDialog.data.pubkey = this.customerPubkey + }, + openCheckout() { + // Check if user is logged in + if (this.customerPubkey) { + this.checkoutDialog.data.pubkey = this.customerPubkey + if (this.customerPrivkey && !useExtension) { + this.checkoutDialog.data.privkey = this.customerPrivkey + } + } + this.checkoutDialog.show = true + }, resetCheckout() { this.checkoutDialog = { show: false, @@ -296,9 +315,9 @@ async function customerStall(path) { } }, created() { - this.customerPubkey = this.account.pubkey - this.customerPrivkey = this.account.privkey - this.customerUseExtension = this.account.useExtension + this.customerPubkey = this.account?.pubkey + this.customerPrivkey = this.account?.privkey + this.customerUseExtension = this.account?.useExtension setTimeout(() => { if (window.nostr) { this.hasNip07 = true From f6e9f47c544f3baae2419e3cdbdf73e8d9448e1c Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Wed, 8 Mar 2023 21:05:53 +0000 Subject: [PATCH 188/891] brush up checkout dialog --- .../components/chat-dialog/chat-dialog.html | 1 - .../customer-stall/customer-stall.html | 94 +++++++++++++++---- .../customer-stall/customer-stall.js | 25 ++++- 3 files changed, 96 insertions(+), 24 deletions(-) diff --git a/static/components/chat-dialog/chat-dialog.html b/static/components/chat-dialog/chat-dialog.html index fd87273..1e46a4d 100644 --- a/static/components/chat-dialog/chat-dialog.html +++ b/static/components/chat-dialog/chat-dialog.html @@ -2,7 +2,6 @@
@@ -62,35 +62,77 @@ v-model.trim="checkoutDialog.data.username" label="Name *optional" > + + + + It seems you haven't logged in. You can: +
    +
  1. + enter your public and private keys bellow (to sign the order + message) +
  2. +
  3. use a Nostr Signer Extension (NIP07)
  4. +
  5. + fill out the required fields, without keys, and download the + order and send as a direct message to the merchant on any + Nostr client +
  6. +
+
+ + Use a Nostr browser extension + Download the order and send manually + +
+
+ -
Download Order + Checkout @@ -154,7 +208,7 @@ position="top" @hide="closeQrCodeDialog" > - +
diff --git a/static/components/customer-stall/customer-stall.js b/static/components/customer-stall/customer-stall.js index 58ab910..b417c91 100644 --- a/static/components/customer-stall/customer-stall.js +++ b/static/components/customer-stall/customer-stall.js @@ -16,6 +16,7 @@ async function customerStall(path) { data: function () { return { loading: false, + isPwd: true, cart: { total: 0, size: 0, @@ -113,6 +114,24 @@ async function customerStall(path) { products: new Map() } }, + async downloadOrder() { + return + }, + async getFromExtension() { + this.customerPubkey = await window.nostr.getPublicKey() + this.customerUseExtension = true + this.checkoutDialog.data.pubkey = this.customerPubkey + }, + openCheckout() { + // Check if user is logged in + if (this.customerPubkey) { + this.checkoutDialog.data.pubkey = this.customerPubkey + if (this.customerPrivkey && !useExtension) { + this.checkoutDialog.data.privkey = this.customerPrivkey + } + } + this.checkoutDialog.show = true + }, resetCheckout() { this.checkoutDialog = { show: false, @@ -296,9 +315,9 @@ async function customerStall(path) { } }, created() { - this.customerPubkey = this.account.pubkey - this.customerPrivkey = this.account.privkey - this.customerUseExtension = this.account.useExtension + this.customerPubkey = this.account?.pubkey + this.customerPrivkey = this.account?.privkey + this.customerUseExtension = this.account?.useExtension setTimeout(() => { if (window.nostr) { this.hasNip07 = true From 448d243838fe25887d4964b479fde54b6bcb4ca1 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Thu, 9 Mar 2023 10:00:55 +0000 Subject: [PATCH 189/891] fixing vlad's review comments --- .../components/chat-dialog/chat-dialog.html | 2 +- static/components/chat-dialog/chat-dialog.js | 57 +------------------ static/js/market.js | 9 +-- static/js/utils.js | 54 ++++++++++++++++++ views.py | 14 +---- 5 files changed, 63 insertions(+), 73 deletions(-) diff --git a/static/components/chat-dialog/chat-dialog.html b/static/components/chat-dialog/chat-dialog.html index 1e46a4d..a428044 100644 --- a/static/components/chat-dialog/chat-dialog.html +++ b/static/components/chat-dialog/chat-dialog.html @@ -33,7 +33,7 @@ :text="[message.msg]" :sent="message.sender == 'Me'" :bg-color="message.sender == 'Me' ? 'white' : 'light-green-2'" - :stamp="`${timeFromNow(message.timestamp * 1000)}`" + :stamp="message.timestamp" size="6" /> diff --git a/static/components/chat-dialog/chat-dialog.js b/static/components/chat-dialog/chat-dialog.js index e89e26d..c111e27 100644 --- a/static/components/chat-dialog/chat-dialog.js +++ b/static/components/chat-dialog/chat-dialog.js @@ -69,7 +69,7 @@ async function chatDialog(path) { } messagesMap.set(event.id, { msg: plaintext, - timestamp: event.created_at, + timestamp: timeFromNow(event.created_at * 1000), sender: `${mine ? 'Me' : 'Merchant'}` }) } catch { @@ -107,11 +107,9 @@ async function chatDialog(path) { let pub = relay.publish(event) pub.on('ok', () => { console.debug(`${relay.url} has accepted our event`) - relay.close() }) pub.on('failed', reason => { console.debug(`failed to publish to ${relay.url}: ${reason}`) - relay.close() }) this.newMessage = '' } catch (e) { @@ -146,59 +144,6 @@ async function chatDialog(path) { event = await window.nostr.signEvent(event) } return event - }, - timeFromNow(time) { - // Get timestamps - let unixTime = new Date(time).getTime() - if (!unixTime) return - let now = new Date().getTime() - - // Calculate difference - let difference = unixTime / 1000 - now / 1000 - - // Setup return object - let tfn = {} - - // Check if time is in the past, present, or future - tfn.when = 'now' - if (difference > 0) { - tfn.when = 'future' - } else if (difference < -1) { - tfn.when = 'past' - } - - // Convert difference to absolute - difference = Math.abs(difference) - - // Calculate time unit - if (difference / (60 * 60 * 24 * 365) > 1) { - // Years - tfn.unitOfTime = 'years' - tfn.time = Math.floor(difference / (60 * 60 * 24 * 365)) - } else if (difference / (60 * 60 * 24 * 45) > 1) { - // Months - tfn.unitOfTime = 'months' - tfn.time = Math.floor(difference / (60 * 60 * 24 * 45)) - } else if (difference / (60 * 60 * 24) > 1) { - // Days - tfn.unitOfTime = 'days' - tfn.time = Math.floor(difference / (60 * 60 * 24)) - } else if (difference / (60 * 60) > 1) { - // Hours - tfn.unitOfTime = 'hours' - tfn.time = Math.floor(difference / (60 * 60)) - } else if (difference / 60 > 1) { - // Minutes - tfn.unitOfTime = 'minutes' - tfn.time = Math.floor(difference / 60) - } else { - // Seconds - tfn.unitOfTime = 'seconds' - tfn.time = Math.floor(difference) - } - - // Return time from now data - return `${tfn.time} ${tfn.unitOfTime}` } }, created() { diff --git a/static/js/market.js b/static/js/market.js index 3cf9b90..52fed25 100644 --- a/static/js/market.js +++ b/static/js/market.js @@ -62,12 +62,13 @@ const market = async () => { if (this.activeStall) { products = products.filter(p => p.stall_id == this.activeStall) } - if (!this.searchText || this.searchText.length < 2) return products + const searchText = this.searchText.toLowerCase() + if (!searchText || searchText.length < 2) return products return products.filter(p => { return ( - p.name.includes(this.searchText) || - p.description.includes(this.searchText) || - p.categories.includes(this.searchText) + p.name.toLowerCase().includes(searchText) || + p.description.toLowerCase().includes(searchText) || + p.categories.toLowerCase().includes(searchText) ) }) }, diff --git a/static/js/utils.js b/static/js/utils.js index c11cdef..7843723 100644 --- a/static/js/utils.js +++ b/static/js/utils.js @@ -38,3 +38,57 @@ function isJson(str) { return false } } + +function timeFromNow(time) { + // Get timestamps + let unixTime = new Date(time).getTime() + if (!unixTime) return + let now = new Date().getTime() + + // Calculate difference + let difference = unixTime / 1000 - now / 1000 + + // Setup return object + let tfn = {} + + // Check if time is in the past, present, or future + tfn.when = 'now' + if (difference > 0) { + tfn.when = 'future' + } else if (difference < -1) { + tfn.when = 'past' + } + + // Convert difference to absolute + difference = Math.abs(difference) + + // Calculate time unit + if (difference / (60 * 60 * 24 * 365) > 1) { + // Years + tfn.unitOfTime = 'years' + tfn.time = Math.floor(difference / (60 * 60 * 24 * 365)) + } else if (difference / (60 * 60 * 24 * 45) > 1) { + // Months + tfn.unitOfTime = 'months' + tfn.time = Math.floor(difference / (60 * 60 * 24 * 45)) + } else if (difference / (60 * 60 * 24) > 1) { + // Days + tfn.unitOfTime = 'days' + tfn.time = Math.floor(difference / (60 * 60 * 24)) + } else if (difference / (60 * 60) > 1) { + // Hours + tfn.unitOfTime = 'hours' + tfn.time = Math.floor(difference / (60 * 60)) + } else if (difference / 60 > 1) { + // Minutes + tfn.unitOfTime = 'minutes' + tfn.time = Math.floor(difference / 60) + } else { + // Seconds + tfn.unitOfTime = 'seconds' + tfn.time = Math.floor(difference) + } + + // Return time from now data + return `${tfn.time} ${tfn.unitOfTime}` +} diff --git a/views.py b/views.py index d83bc85..3b757fb 100644 --- a/views.py +++ b/views.py @@ -23,18 +23,8 @@ async def index(request: Request, user: User = Depends(check_user_exists)): @nostrmarket_ext.get("/market", response_class=HTMLResponse) -async def market( - request: Request, - stall_id: str = Query(None), - product_id: str = Query(None), - merchant_pubkey: str = Query(None), -): +async def market(request: Request): return nostrmarket_renderer().TemplateResponse( "nostrmarket/market.html", - { - "request": request, - "stall_id": stall_id, - "product_id": product_id, - "merchant_pubkey": merchant_pubkey, - }, + {"request": request}, ) From 039b4e23cd123dee534b05c61a55b6036417488a Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Thu, 9 Mar 2023 10:00:55 +0000 Subject: [PATCH 190/891] fixing vlad's review comments --- .../components/chat-dialog/chat-dialog.html | 2 +- static/components/chat-dialog/chat-dialog.js | 57 +------------------ static/js/market.js | 9 +-- static/js/utils.js | 54 ++++++++++++++++++ views.py | 14 +---- 5 files changed, 63 insertions(+), 73 deletions(-) diff --git a/static/components/chat-dialog/chat-dialog.html b/static/components/chat-dialog/chat-dialog.html index 1e46a4d..a428044 100644 --- a/static/components/chat-dialog/chat-dialog.html +++ b/static/components/chat-dialog/chat-dialog.html @@ -33,7 +33,7 @@ :text="[message.msg]" :sent="message.sender == 'Me'" :bg-color="message.sender == 'Me' ? 'white' : 'light-green-2'" - :stamp="`${timeFromNow(message.timestamp * 1000)}`" + :stamp="message.timestamp" size="6" /> diff --git a/static/components/chat-dialog/chat-dialog.js b/static/components/chat-dialog/chat-dialog.js index e89e26d..c111e27 100644 --- a/static/components/chat-dialog/chat-dialog.js +++ b/static/components/chat-dialog/chat-dialog.js @@ -69,7 +69,7 @@ async function chatDialog(path) { } messagesMap.set(event.id, { msg: plaintext, - timestamp: event.created_at, + timestamp: timeFromNow(event.created_at * 1000), sender: `${mine ? 'Me' : 'Merchant'}` }) } catch { @@ -107,11 +107,9 @@ async function chatDialog(path) { let pub = relay.publish(event) pub.on('ok', () => { console.debug(`${relay.url} has accepted our event`) - relay.close() }) pub.on('failed', reason => { console.debug(`failed to publish to ${relay.url}: ${reason}`) - relay.close() }) this.newMessage = '' } catch (e) { @@ -146,59 +144,6 @@ async function chatDialog(path) { event = await window.nostr.signEvent(event) } return event - }, - timeFromNow(time) { - // Get timestamps - let unixTime = new Date(time).getTime() - if (!unixTime) return - let now = new Date().getTime() - - // Calculate difference - let difference = unixTime / 1000 - now / 1000 - - // Setup return object - let tfn = {} - - // Check if time is in the past, present, or future - tfn.when = 'now' - if (difference > 0) { - tfn.when = 'future' - } else if (difference < -1) { - tfn.when = 'past' - } - - // Convert difference to absolute - difference = Math.abs(difference) - - // Calculate time unit - if (difference / (60 * 60 * 24 * 365) > 1) { - // Years - tfn.unitOfTime = 'years' - tfn.time = Math.floor(difference / (60 * 60 * 24 * 365)) - } else if (difference / (60 * 60 * 24 * 45) > 1) { - // Months - tfn.unitOfTime = 'months' - tfn.time = Math.floor(difference / (60 * 60 * 24 * 45)) - } else if (difference / (60 * 60 * 24) > 1) { - // Days - tfn.unitOfTime = 'days' - tfn.time = Math.floor(difference / (60 * 60 * 24)) - } else if (difference / (60 * 60) > 1) { - // Hours - tfn.unitOfTime = 'hours' - tfn.time = Math.floor(difference / (60 * 60)) - } else if (difference / 60 > 1) { - // Minutes - tfn.unitOfTime = 'minutes' - tfn.time = Math.floor(difference / 60) - } else { - // Seconds - tfn.unitOfTime = 'seconds' - tfn.time = Math.floor(difference) - } - - // Return time from now data - return `${tfn.time} ${tfn.unitOfTime}` } }, created() { diff --git a/static/js/market.js b/static/js/market.js index 3cf9b90..52fed25 100644 --- a/static/js/market.js +++ b/static/js/market.js @@ -62,12 +62,13 @@ const market = async () => { if (this.activeStall) { products = products.filter(p => p.stall_id == this.activeStall) } - if (!this.searchText || this.searchText.length < 2) return products + const searchText = this.searchText.toLowerCase() + if (!searchText || searchText.length < 2) return products return products.filter(p => { return ( - p.name.includes(this.searchText) || - p.description.includes(this.searchText) || - p.categories.includes(this.searchText) + p.name.toLowerCase().includes(searchText) || + p.description.toLowerCase().includes(searchText) || + p.categories.toLowerCase().includes(searchText) ) }) }, diff --git a/static/js/utils.js b/static/js/utils.js index c11cdef..7843723 100644 --- a/static/js/utils.js +++ b/static/js/utils.js @@ -38,3 +38,57 @@ function isJson(str) { return false } } + +function timeFromNow(time) { + // Get timestamps + let unixTime = new Date(time).getTime() + if (!unixTime) return + let now = new Date().getTime() + + // Calculate difference + let difference = unixTime / 1000 - now / 1000 + + // Setup return object + let tfn = {} + + // Check if time is in the past, present, or future + tfn.when = 'now' + if (difference > 0) { + tfn.when = 'future' + } else if (difference < -1) { + tfn.when = 'past' + } + + // Convert difference to absolute + difference = Math.abs(difference) + + // Calculate time unit + if (difference / (60 * 60 * 24 * 365) > 1) { + // Years + tfn.unitOfTime = 'years' + tfn.time = Math.floor(difference / (60 * 60 * 24 * 365)) + } else if (difference / (60 * 60 * 24 * 45) > 1) { + // Months + tfn.unitOfTime = 'months' + tfn.time = Math.floor(difference / (60 * 60 * 24 * 45)) + } else if (difference / (60 * 60 * 24) > 1) { + // Days + tfn.unitOfTime = 'days' + tfn.time = Math.floor(difference / (60 * 60 * 24)) + } else if (difference / (60 * 60) > 1) { + // Hours + tfn.unitOfTime = 'hours' + tfn.time = Math.floor(difference / (60 * 60)) + } else if (difference / 60 > 1) { + // Minutes + tfn.unitOfTime = 'minutes' + tfn.time = Math.floor(difference / 60) + } else { + // Seconds + tfn.unitOfTime = 'seconds' + tfn.time = Math.floor(difference) + } + + // Return time from now data + return `${tfn.time} ${tfn.unitOfTime}` +} diff --git a/views.py b/views.py index d83bc85..3b757fb 100644 --- a/views.py +++ b/views.py @@ -23,18 +23,8 @@ async def index(request: Request, user: User = Depends(check_user_exists)): @nostrmarket_ext.get("/market", response_class=HTMLResponse) -async def market( - request: Request, - stall_id: str = Query(None), - product_id: str = Query(None), - merchant_pubkey: str = Query(None), -): +async def market(request: Request): return nostrmarket_renderer().TemplateResponse( "nostrmarket/market.html", - { - "request": request, - "stall_id": stall_id, - "product_id": product_id, - "merchant_pubkey": merchant_pubkey, - }, + {"request": request}, ) From b4b1b308b2eaf841b03ed22fd0a0f266416a6f73 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Tue, 28 Feb 2023 14:45:45 +0000 Subject: [PATCH 191/891] initial files --- templates/nostrmarket/market.html | 202 ++++++++++++++++++++++++++++++ templates/nostrmarket/stall.html | 61 +++++++++ views.py | 18 +++ 3 files changed, 281 insertions(+) create mode 100644 templates/nostrmarket/market.html create mode 100644 templates/nostrmarket/stall.html diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html new file mode 100644 index 0000000..ef29755 --- /dev/null +++ b/templates/nostrmarket/market.html @@ -0,0 +1,202 @@ +{% extends "public.html" %} {% block page %} +
+
+ +
+ Market: +
+
+ + + +
+
+
+
+
+
+ + {% 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/templates/nostrmarket/stall.html b/templates/nostrmarket/stall.html new file mode 100644 index 0000000..208938c --- /dev/null +++ b/templates/nostrmarket/stall.html @@ -0,0 +1,61 @@ +{% extends "public.html" %} {% block page %} +
+{% endblock %} {% block scripts %} + + +{% endblock %} diff --git a/views.py b/views.py index ca8e1f7..47b07ff 100644 --- a/views.py +++ b/views.py @@ -20,3 +20,21 @@ async def index(request: Request, user: User = Depends(check_user_exists)): "nostrmarket/index.html", {"request": request, "user": user.dict()}, ) + + +@nostrmarket_ext.get("/market", response_class=HTMLResponse) +async def market(request: Request): + return nostrmarket_renderer().TemplateResponse( + "nostrmarket/market.html", + { + "request": request, + }, + ) + + +@nostrmarket_ext.get("/stall/{stall_id}", response_class=HTMLResponse) +async def stall(request: Request, stall_id: str): + return nostrmarket_renderer().TemplateResponse( + "nostrmarket/stall.html", + {"request": request, "stall_id": stall_id}, + ) From 296c3915ead4fa6031b4b3358fc139d4f403ff9e Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Tue, 28 Feb 2023 14:45:45 +0000 Subject: [PATCH 192/891] initial files --- templates/nostrmarket/market.html | 202 ++++++++++++++++++++++++++++++ templates/nostrmarket/stall.html | 61 +++++++++ views.py | 18 +++ 3 files changed, 281 insertions(+) create mode 100644 templates/nostrmarket/market.html create mode 100644 templates/nostrmarket/stall.html diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html new file mode 100644 index 0000000..ef29755 --- /dev/null +++ b/templates/nostrmarket/market.html @@ -0,0 +1,202 @@ +{% extends "public.html" %} {% block page %} +
+
+ +
+ Market: +
+
+ + + +
+
+
+
+
+
+ + {% 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/templates/nostrmarket/stall.html b/templates/nostrmarket/stall.html new file mode 100644 index 0000000..208938c --- /dev/null +++ b/templates/nostrmarket/stall.html @@ -0,0 +1,61 @@ +{% extends "public.html" %} {% block page %} +
+{% endblock %} {% block scripts %} + + +{% endblock %} diff --git a/views.py b/views.py index ca8e1f7..47b07ff 100644 --- a/views.py +++ b/views.py @@ -20,3 +20,21 @@ async def index(request: Request, user: User = Depends(check_user_exists)): "nostrmarket/index.html", {"request": request, "user": user.dict()}, ) + + +@nostrmarket_ext.get("/market", response_class=HTMLResponse) +async def market(request: Request): + return nostrmarket_renderer().TemplateResponse( + "nostrmarket/market.html", + { + "request": request, + }, + ) + + +@nostrmarket_ext.get("/stall/{stall_id}", response_class=HTMLResponse) +async def stall(request: Request, stall_id: str): + return nostrmarket_renderer().TemplateResponse( + "nostrmarket/stall.html", + {"request": request, "stall_id": stall_id}, + ) From 63d1783fa489ab860026ccdd575f77dd5a911d4d Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Tue, 28 Feb 2023 16:00:57 +0000 Subject: [PATCH 193/891] get nostr events and display --- templates/nostrmarket/market.html | 299 ++++++++++++++++++------------ 1 file changed, 182 insertions(+), 117 deletions(-) diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html index ef29755..eb18f39 100644 --- a/templates/nostrmarket/market.html +++ b/templates/nostrmarket/market.html @@ -1,113 +1,162 @@ {% extends "public.html" %} {% block page %} -
-
- -
- Market: -
-
- - - -
+ + + + Settings -
-
-
-
- - {% 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 }} - + + Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quidem, + eius reprehenderit eos corrupti commodi magni quaerat ex numquam, + dolorum officiis modi facere maiores architecto suscipit iste + eveniet doloribus ullam aliquid. + +
+ + - Visit Stall - - - {% endraw %} - -
-
+ + + Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quidem, + eius reprehenderit eos corrupti commodi magni quaerat ex numquam, + dolorum officiis modi facere maiores architecto suscipit iste + eveniet doloribus ullam aliquid. + + + + +
+ + +
+
+ +
+ Market: +
+
+ + + +
+
+
+
+
+
+ + {% 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 %} From 69d699672c2f536850b8cb94654f02b305321f6e Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Tue, 28 Feb 2023 16:00:57 +0000 Subject: [PATCH 194/891] get nostr events and display --- templates/nostrmarket/market.html | 299 ++++++++++++++++++------------ 1 file changed, 182 insertions(+), 117 deletions(-) diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html index ef29755..eb18f39 100644 --- a/templates/nostrmarket/market.html +++ b/templates/nostrmarket/market.html @@ -1,113 +1,162 @@ {% extends "public.html" %} {% block page %} -
-
- -
- Market: -
-
- - - -
+ + + + Settings -
-
-
-
- - {% 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 }} - + + Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quidem, + eius reprehenderit eos corrupti commodi magni quaerat ex numquam, + dolorum officiis modi facere maiores architecto suscipit iste + eveniet doloribus ullam aliquid. + +
+ + - Visit Stall - - - {% endraw %} - -
-
+ + + Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quidem, + eius reprehenderit eos corrupti commodi magni quaerat ex numquam, + dolorum officiis modi facere maiores architecto suscipit iste + eveniet doloribus ullam aliquid. + + + + +
+ + +
+
+ +
+ Market: +
+
+ + + +
+
+
+
+
+
+ + {% 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 %} From 2c5207a2008046a93c4b88e097c8793b99750678 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Tue, 28 Feb 2023 17:06:24 +0000 Subject: [PATCH 195/891] menu add pubkey/npub --- templates/nostrmarket/market.html | 120 +++++++++++++++++++++++++++--- 1 file changed, 108 insertions(+), 12 deletions(-) diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html index eb18f39..33af581 100644 --- a/templates/nostrmarket/market.html +++ b/templates/nostrmarket/market.html @@ -14,10 +14,44 @@ > - Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quidem, - eius reprehenderit eos corrupti commodi magni quaerat ex numquam, - dolorum officiis modi facere maiores architecto suscipit iste - eveniet doloribus ullam aliquid. + + + + + + + + + + + {%raw%} + + {{ `${pub.slice(0, 5)}...${pub.slice(-5)}` }}{{ pub }} + + + + + {%endraw%} + + @@ -29,10 +63,35 @@ > - Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quidem, - eius reprehenderit eos corrupti commodi magni quaerat ex numquam, - dolorum officiis modi facere maiores architecto suscipit iste - eveniet doloribus ullam aliquid. + + + + + + {%raw%} + + {{ url }} + + + + + {%endraw%} + + @@ -190,11 +249,14 @@ return { drawer: true, pubkeys: new Set(), + relays: new Set(defaultRelays), stalls: [], products: [], events: [], searchText: null, - exchangeRates: null + exchangeRates: null, + inputPubkey: null, + inputRelay: null } }, computed: { @@ -220,7 +282,7 @@ async initNostr() { this.pool = new nostr.SimplePool() this.relays = new Set(defaultRelays) - await this.pool + let sub = await this.pool .list(Array.from(this.relays), [ { kinds: [30005], @@ -236,11 +298,12 @@ } else { // it's a stall this.stalls.push(e.content) + return } }) - console.log(this.stalls) - console.log(this.products) }) + await Promise.resolve(sub) + this.pool.close() }, async getRates() { let noFiat = this.stalls.map(s => s.currency).every(c => c == 'sat') @@ -260,8 +323,41 @@ }, getAmountFormated(amount, unit = 'USD') { return LNbits.utils.formatCurrency(amount, unit) + }, + addPubkey() { + let pubkey = String(this.inputPubkey).trim() + let regExp = /^#([0-9a-f]{3}){1,2}$/i + if (regExp.test(pubkey)) { + return this.pubkeys.add(pubkey) + } + try { + let {type, data} = nostr.nip19.decode(pubkey) + if (type === 'npub') pubkey = data + else if (type === 'nprofile') { + pubkey = data.pubkey + givenRelays = data.relays + } + this.pubkeys.add(pubkey) + this.inputPubkey = null + } catch (err) { + console.error(err) + } + }, + addRelay() { + let relay = String(this.inputRelay).trim() + if (!relay.startsWith('ws')) { + console.debug('invalid url') + return + } + this.relays.add(relay) + this.inputRelay = null } } }) + {% endblock %} From de506b1111dad2c4fcf20c114f7d2f2ffa36c3b0 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Tue, 28 Feb 2023 17:06:24 +0000 Subject: [PATCH 196/891] menu add pubkey/npub --- templates/nostrmarket/market.html | 120 +++++++++++++++++++++++++++--- 1 file changed, 108 insertions(+), 12 deletions(-) diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html index eb18f39..33af581 100644 --- a/templates/nostrmarket/market.html +++ b/templates/nostrmarket/market.html @@ -14,10 +14,44 @@ > - Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quidem, - eius reprehenderit eos corrupti commodi magni quaerat ex numquam, - dolorum officiis modi facere maiores architecto suscipit iste - eveniet doloribus ullam aliquid. + + + + + + + + + + + {%raw%} + + {{ `${pub.slice(0, 5)}...${pub.slice(-5)}` }}{{ pub }} + + + + + {%endraw%} + + @@ -29,10 +63,35 @@ > - Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quidem, - eius reprehenderit eos corrupti commodi magni quaerat ex numquam, - dolorum officiis modi facere maiores architecto suscipit iste - eveniet doloribus ullam aliquid. + + + + + + {%raw%} + + {{ url }} + + + + + {%endraw%} + + @@ -190,11 +249,14 @@ return { drawer: true, pubkeys: new Set(), + relays: new Set(defaultRelays), stalls: [], products: [], events: [], searchText: null, - exchangeRates: null + exchangeRates: null, + inputPubkey: null, + inputRelay: null } }, computed: { @@ -220,7 +282,7 @@ async initNostr() { this.pool = new nostr.SimplePool() this.relays = new Set(defaultRelays) - await this.pool + let sub = await this.pool .list(Array.from(this.relays), [ { kinds: [30005], @@ -236,11 +298,12 @@ } else { // it's a stall this.stalls.push(e.content) + return } }) - console.log(this.stalls) - console.log(this.products) }) + await Promise.resolve(sub) + this.pool.close() }, async getRates() { let noFiat = this.stalls.map(s => s.currency).every(c => c == 'sat') @@ -260,8 +323,41 @@ }, getAmountFormated(amount, unit = 'USD') { return LNbits.utils.formatCurrency(amount, unit) + }, + addPubkey() { + let pubkey = String(this.inputPubkey).trim() + let regExp = /^#([0-9a-f]{3}){1,2}$/i + if (regExp.test(pubkey)) { + return this.pubkeys.add(pubkey) + } + try { + let {type, data} = nostr.nip19.decode(pubkey) + if (type === 'npub') pubkey = data + else if (type === 'nprofile') { + pubkey = data.pubkey + givenRelays = data.relays + } + this.pubkeys.add(pubkey) + this.inputPubkey = null + } catch (err) { + console.error(err) + } + }, + addRelay() { + let relay = String(this.inputRelay).trim() + if (!relay.startsWith('ws')) { + console.debug('invalid url') + return + } + this.relays.add(relay) + this.inputRelay = null } } }) + {% endblock %} From 85d680ad3edf11a036ffd781576ab9f515056aba Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Tue, 28 Feb 2023 21:04:29 +0000 Subject: [PATCH 197/891] remove pubkeys and relays --- templates/nostrmarket/market.html | 36 ++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html index 33af581..103d7db 100644 --- a/templates/nostrmarket/market.html +++ b/templates/nostrmarket/market.html @@ -1,6 +1,6 @@ {% extends "public.html" %} {% block page %} - - + + Settings @@ -47,6 +47,7 @@ dense round icon="delete" + @click="removePubkey(pub)" /> {%endraw%} @@ -87,6 +88,7 @@ dense round icon="delete" + @click="removeRelay(url)" /> {%endraw%} @@ -212,7 +214,7 @@
- + @@ -247,9 +249,9 @@ mixins: [windowMixin], data: function () { return { - drawer: true, + drawer: false, pubkeys: new Set(), - relays: new Set(defaultRelays), + relays: new Set(), stalls: [], products: [], events: [], @@ -269,6 +271,9 @@ p.categories.includes(this.searchText) ) }) + }, + relayList() { + return Array.from(this.relays) } }, async created() { @@ -276,12 +281,12 @@ this.pubkeys.add( '855ea22a88d7df7ccd8497777db81f115575d5362f51df3af02ead383f5eaba2' ) - //await this.initNostr() + this.relays = new Set(defaultRelays) + await this.initNostr() }, methods: { async initNostr() { this.pool = new nostr.SimplePool() - this.relays = new Set(defaultRelays) let sub = await this.pool .list(Array.from(this.relays), [ { @@ -343,6 +348,12 @@ console.error(err) } }, + removePubkey(pubkey) { + // Needs a hack for Vue reactivity + let pubkeys = this.pubkeys + pubkeys.delete(pubkey) + this.pubkeys = new Set(Array.from(pubkeys)) + }, addRelay() { let relay = String(this.inputRelay).trim() if (!relay.startsWith('ws')) { @@ -351,13 +362,14 @@ } this.relays.add(relay) this.inputRelay = null + }, + removeRelay(relay) { + // Needs a hack for Vue reactivity + let relays = this.relays + relays.delete(relay) + this.relays = new Set(Array.from(relays)) } } }) - {% endblock %} From af0d2774b995950ffe4e75e1ac6be122da36c33c Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Tue, 28 Feb 2023 21:04:29 +0000 Subject: [PATCH 198/891] remove pubkeys and relays --- templates/nostrmarket/market.html | 36 ++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html index 33af581..103d7db 100644 --- a/templates/nostrmarket/market.html +++ b/templates/nostrmarket/market.html @@ -1,6 +1,6 @@ {% extends "public.html" %} {% block page %} - - + + Settings @@ -47,6 +47,7 @@ dense round icon="delete" + @click="removePubkey(pub)" /> {%endraw%} @@ -87,6 +88,7 @@ dense round icon="delete" + @click="removeRelay(url)" /> {%endraw%} @@ -212,7 +214,7 @@
- + @@ -247,9 +249,9 @@ mixins: [windowMixin], data: function () { return { - drawer: true, + drawer: false, pubkeys: new Set(), - relays: new Set(defaultRelays), + relays: new Set(), stalls: [], products: [], events: [], @@ -269,6 +271,9 @@ p.categories.includes(this.searchText) ) }) + }, + relayList() { + return Array.from(this.relays) } }, async created() { @@ -276,12 +281,12 @@ this.pubkeys.add( '855ea22a88d7df7ccd8497777db81f115575d5362f51df3af02ead383f5eaba2' ) - //await this.initNostr() + this.relays = new Set(defaultRelays) + await this.initNostr() }, methods: { async initNostr() { this.pool = new nostr.SimplePool() - this.relays = new Set(defaultRelays) let sub = await this.pool .list(Array.from(this.relays), [ { @@ -343,6 +348,12 @@ console.error(err) } }, + removePubkey(pubkey) { + // Needs a hack for Vue reactivity + let pubkeys = this.pubkeys + pubkeys.delete(pubkey) + this.pubkeys = new Set(Array.from(pubkeys)) + }, addRelay() { let relay = String(this.inputRelay).trim() if (!relay.startsWith('ws')) { @@ -351,13 +362,14 @@ } this.relays.add(relay) this.inputRelay = null + }, + removeRelay(relay) { + // Needs a hack for Vue reactivity + let relays = this.relays + relays.delete(relay) + this.relays = new Set(Array.from(relays)) } } }) - {% endblock %} From e251cce9ef2f00bcd77e204e9b1a168ab214b44f Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Tue, 28 Feb 2023 22:38:50 +0000 Subject: [PATCH 199/891] fetch profiles --- templates/nostrmarket/market.html | 104 +++++++++++++++++++----------- 1 file changed, 66 insertions(+), 38 deletions(-) diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html index 103d7db..a85f2fc 100644 --- a/templates/nostrmarket/market.html +++ b/templates/nostrmarket/market.html @@ -24,20 +24,31 @@ > - + + {%raw%} - + + - {%raw%} {{ `${pub.slice(0, 5)}...${pub.slice(-5)}` }}{{ pub }}{{ profiles.get(pub).name }} + {{ `${pub.slice(0, 5)}...${pub.slice(-5)}` + }} + {{ pub }} {% raw %} { this.events = events || [] this.events.map(eventToObj).map(e => { - if (e.content.stall) { - //it's a product - this.products.push(e.content) + if (e.kind == 0) { + this.profiles.set(e.pubkey, e.content) + return + } else if (e.content.stall) { + //it's a product `d` is the prod. id + products.set(e.d, e.content) } else { - // it's a stall - this.stalls.push(e.content) + // it's a stall `d` is the stall id + stalls.set(e.d, e.content) return } }) }) await Promise.resolve(sub) - this.pool.close() + this.products = Array.from(products.values()) + this.stalls = Array.from(stalls.values()) + pool.close(relays) }, async getRates() { let noFiat = this.stalls.map(s => s.currency).every(c => c == 'sat') @@ -329,32 +352,36 @@ getAmountFormated(amount, unit = 'USD') { return LNbits.utils.formatCurrency(amount, unit) }, - addPubkey() { + async addPubkey() { let pubkey = String(this.inputPubkey).trim() let regExp = /^#([0-9a-f]{3}){1,2}$/i - if (regExp.test(pubkey)) { - return this.pubkeys.add(pubkey) - } - try { - let {type, data} = nostr.nip19.decode(pubkey) - if (type === 'npub') pubkey = data - else if (type === 'nprofile') { - pubkey = data.pubkey - givenRelays = data.relays + if (pubkey.startsWith('n')) { + try { + let {type, data} = nostr.nip19.decode(pubkey) + if (type === 'npub') pubkey = data + else if (type === 'nprofile') { + pubkey = data.pubkey + givenRelays = data.relays + } + this.pubkeys.add(pubkey) + this.inputPubkey = null + } catch (err) { + console.error(err) } - this.pubkeys.add(pubkey) - this.inputPubkey = null - } catch (err) { - console.error(err) + } else if (regExp.test(pubkey)) { + pubkey = pubkey } + this.pubkeys.add(pubkey) + await this.initNostr() }, removePubkey(pubkey) { // Needs a hack for Vue reactivity let pubkeys = this.pubkeys pubkeys.delete(pubkey) + this.profiles.delete(pubkey) this.pubkeys = new Set(Array.from(pubkeys)) }, - addRelay() { + async addRelay() { let relay = String(this.inputRelay).trim() if (!relay.startsWith('ws')) { console.debug('invalid url') @@ -362,6 +389,7 @@ } this.relays.add(relay) this.inputRelay = null + await this.initNostr() }, removeRelay(relay) { // Needs a hack for Vue reactivity From c58af7e233485c8b77310549212c4c94e396c589 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Tue, 28 Feb 2023 22:38:50 +0000 Subject: [PATCH 200/891] fetch profiles --- templates/nostrmarket/market.html | 104 +++++++++++++++++++----------- 1 file changed, 66 insertions(+), 38 deletions(-) diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html index 103d7db..a85f2fc 100644 --- a/templates/nostrmarket/market.html +++ b/templates/nostrmarket/market.html @@ -24,20 +24,31 @@ > - + + {%raw%} - + + - {%raw%} {{ `${pub.slice(0, 5)}...${pub.slice(-5)}` }}{{ pub }}{{ profiles.get(pub).name }} + {{ `${pub.slice(0, 5)}...${pub.slice(-5)}` + }} + {{ pub }} {% raw %} { this.events = events || [] this.events.map(eventToObj).map(e => { - if (e.content.stall) { - //it's a product - this.products.push(e.content) + if (e.kind == 0) { + this.profiles.set(e.pubkey, e.content) + return + } else if (e.content.stall) { + //it's a product `d` is the prod. id + products.set(e.d, e.content) } else { - // it's a stall - this.stalls.push(e.content) + // it's a stall `d` is the stall id + stalls.set(e.d, e.content) return } }) }) await Promise.resolve(sub) - this.pool.close() + this.products = Array.from(products.values()) + this.stalls = Array.from(stalls.values()) + pool.close(relays) }, async getRates() { let noFiat = this.stalls.map(s => s.currency).every(c => c == 'sat') @@ -329,32 +352,36 @@ getAmountFormated(amount, unit = 'USD') { return LNbits.utils.formatCurrency(amount, unit) }, - addPubkey() { + async addPubkey() { let pubkey = String(this.inputPubkey).trim() let regExp = /^#([0-9a-f]{3}){1,2}$/i - if (regExp.test(pubkey)) { - return this.pubkeys.add(pubkey) - } - try { - let {type, data} = nostr.nip19.decode(pubkey) - if (type === 'npub') pubkey = data - else if (type === 'nprofile') { - pubkey = data.pubkey - givenRelays = data.relays + if (pubkey.startsWith('n')) { + try { + let {type, data} = nostr.nip19.decode(pubkey) + if (type === 'npub') pubkey = data + else if (type === 'nprofile') { + pubkey = data.pubkey + givenRelays = data.relays + } + this.pubkeys.add(pubkey) + this.inputPubkey = null + } catch (err) { + console.error(err) } - this.pubkeys.add(pubkey) - this.inputPubkey = null - } catch (err) { - console.error(err) + } else if (regExp.test(pubkey)) { + pubkey = pubkey } + this.pubkeys.add(pubkey) + await this.initNostr() }, removePubkey(pubkey) { // Needs a hack for Vue reactivity let pubkeys = this.pubkeys pubkeys.delete(pubkey) + this.profiles.delete(pubkey) this.pubkeys = new Set(Array.from(pubkeys)) }, - addRelay() { + async addRelay() { let relay = String(this.inputRelay).trim() if (!relay.startsWith('ws')) { console.debug('invalid url') @@ -362,6 +389,7 @@ } this.relays.add(relay) this.inputRelay = null + await this.initNostr() }, removeRelay(relay) { // Needs a hack for Vue reactivity From 8510fcecea909b0e90d4270e7a32fd170b452557 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Tue, 28 Feb 2023 22:39:05 +0000 Subject: [PATCH 201/891] avatar picture --- static/images/blank-avatar.webp | Bin 0 -> 11090 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 static/images/blank-avatar.webp diff --git a/static/images/blank-avatar.webp b/static/images/blank-avatar.webp new file mode 100644 index 0000000000000000000000000000000000000000..513b0f3279d36569ead8f6553506c30fbb610703 GIT binary patch literal 11090 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 ASO5S3 literal 0 HcmV?d00001 From ec98664610defa03ca88286a1b000f4bc66fc7e0 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Tue, 28 Feb 2023 22:39:05 +0000 Subject: [PATCH 202/891] avatar picture --- static/images/blank-avatar.webp | Bin 0 -> 11090 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 static/images/blank-avatar.webp diff --git a/static/images/blank-avatar.webp b/static/images/blank-avatar.webp new file mode 100644 index 0000000000000000000000000000000000000000..513b0f3279d36569ead8f6553506c30fbb610703 GIT binary patch literal 11090 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 ASO5S3 literal 0 HcmV?d00001 From 83640c49a2fbc137fb8918e35ff463629d327cd2 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Wed, 1 Mar 2023 16:59:59 +0000 Subject: [PATCH 203/891] market component (ready until final product object) --- .../customer-market/customer-market.html | 83 ++++++++ .../customer-market/customer-market.js | 17 ++ templates/nostrmarket/market.html | 180 +++++++----------- views.py | 10 +- 4 files changed, 169 insertions(+), 121 deletions(-) create mode 100644 static/components/customer-market/customer-market.html create mode 100644 static/components/customer-market/customer-market.js diff --git a/static/components/customer-market/customer-market.html b/static/components/customer-market/customer-market.html new file mode 100644 index 0000000..99af972 --- /dev/null +++ b/static/components/customer-market/customer-market.html @@ -0,0 +1,83 @@ +
+
+ + + + +
+
+ {{ item.product }} +
+
+ + +
+ + +
+
+ {{ item.stallName }} +
+ + {{ item.price }} satsBTC {{ (item.price / 1e8).toFixed(8) }} + + + {{ item.formatedPrice }} + ({{ item.priceInSats }} sats) + + {{ item.amount }} left +
+
+ {{cat}} +
+
+

{{ item.description }}

+
+
+ + + + + Stall: {{ item.stallName }} +
+ See product + + Visit Stall + +
+
+
+
+
diff --git a/static/components/customer-market/customer-market.js b/static/components/customer-market/customer-market.js new file mode 100644 index 0000000..5844105 --- /dev/null +++ b/static/components/customer-market/customer-market.js @@ -0,0 +1,17 @@ +async function customerMarket(path) { + const template = await loadTemplateAsync(path) + Vue.component('customer-market', { + name: 'customer-market', + template, + + props: ['products', 'exchange-rates'], + data: function () { + return {} + }, + methods: { + changePage() { + return + } + } + }) +} diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html index a85f2fc..dba048d 100644 --- a/templates/nostrmarket/market.html +++ b/templates/nostrmarket/market.html @@ -3,6 +3,7 @@ Settings +
@@ -83,7 +84,7 @@ label="Relay URL" hint="Add relays" > - + @@ -114,123 +115,42 @@
- -
- Market: -
-
- - - -
+ + + {%raw%} + + {{ activePage }} + + {%endraw%} + + +
-
-
- - {% 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 %} + + + + + + + + + + + - - Vue.component(VueQrcode.name, VueQrcode) - - Promise.all([ - customerMarket('static/components/customer-market/customer-market.html'), - customerStall('static/components/customer-stall/customer-stall.html'), - productDetail('static/components/product-detail/product-detail.html') - ]) - - new Vue({ - el: '#vue', - mixins: [windowMixin], - data: function () { - return { - drawer: false, - pubkeys: new Set(), - relays: new Set(), - events: [], - stalls: [], - products: [], - profiles: new Map(), - searchText: null, - exchangeRates: null, - inputPubkey: null, - inputRelay: null, - activePage: 'market', - activeStall: null, - activeProduct: null - } - }, - computed: { - filterProducts() { - let products = this.products - if (this.activeStall) { - products = products.filter(p => p.stall == this.activeStall) - } - if (!this.searchText || this.searchText.length < 2) return products - return products.filter(p => { - return ( - p.name.includes(this.searchText) || - p.description.includes(this.searchText) || - p.categories.includes(this.searchText) - ) - }) - }, - stallName() { - return this.stalls.find(s => s.id == this.activeStall)?.name || 'Stall' - }, - productName() { - return ( - this.products.find(p => p.id == this.activeProduct)?.name || 'Product' - ) - } - }, - async created() { - // Check for stored merchants and relays on localStorage - try { - let merchants = this.$q.localStorage.getItem(`diagonAlley.merchants`) - let relays = this.$q.localStorage.getItem(`diagonAlley.relays`) - if (merchants && merchants.length) { - this.pubkeys = new Set(merchants) - } - if (relays && relays.length) { - this.relays = new Set([...defaultRelays, ...relays]) - } - } catch (e) { - console.error(e) - } - // Hardcode pubkeys for testing - /* - this.pubkeys.add( - 'c1415f950a1e3431de2bc5ee35144639e2f514cf158279abff9ed77d50118796' - ) - this.pubkeys.add( - '8f69ac99b96f7c4ad58b98cc38fe5d35ce02daefae7d1609c797ce3b4f92f5fd' - ) - */ - // stall ids S4hQgtTwiF5kGJZPbqMH9M jkCbdtkXeMjGBY3LBf8yn4 - let merchant_pubkey = JSON.parse('{{ merchant_pubkey | tojson }}') - let stall_id = JSON.parse('{{ stall_id | tojson }}') - let product_id = JSON.parse('{{ product_id | tojson }}') - if (merchant_pubkey) { - await addPubkey(merchant_pubkey) - /*LNbits.utils - .confirmDialog( - `We found a merchant pubkey in your request. Do you want to add it to the merchants list?` - ) - .onCancel(() => {}) - .onDismiss(() => {}) - .onOk(() => { - this.pubkeys.add(merchant_pubkey) - })*/ - } - this.$q.loading.show() - this.relays = new Set(defaultRelays) - // Get notes from Nostr - await this.initNostr() - - // What component to render on start - if (stall_id) { - if (product_id) { - this.activeProduct = product_id - } - this.activePage = 'stall' - this.activeStall = stall_id - } - - this.$q.loading.hide() - }, - methods: { - async initNostr() { - const pool = new nostr.SimplePool() - let relays = Array.from(this.relays) - let products = new Map() - let stalls = new Map() - // Get metadata and market data from the pubkeys - let sub = await pool - .list(relays, [ - { - kinds: [0, 30017, 30018], // for production kind is 30017 - authors: Array.from(this.pubkeys) - } - ]) - .then(events => { - console.log(events) - this.events = events || [] - this.events.map(eventToObj).map(e => { - if (e.kind == 0) { - this.profiles.set(e.pubkey, e.content) - return - } else if (e.kind == 30018) { - //it's a product `d` is the prod. id - products.set(e.d, {...e.content, id: e.d, categories: e.t}) - } else if (e.kind == 30017) { - // it's a stall `d` is the stall id - stalls.set(e.d, {...e.content, id: e.d, pubkey: e.pubkey}) - return - } - }) - }) - await Promise.resolve(sub) - this.stalls = await Array.from(stalls.values()) - - this.products = Array.from(products.values()).map(obj => { - let stall = this.stalls.find(s => s.id == obj.stall_id) - obj.stallName = stall.name - if (obj.currency != 'sat') { - obj.formatedPrice = this.getAmountFormated(obj.price, obj.currency) - obj.priceInSats = this.getValueInSats(obj.price, obj.currency) - } - return obj - }) - - pool.close(relays) - }, - async getRates() { - let noFiat = this.stalls.map(s => s.currency).every(c => c == 'sat') - if (noFiat) return - try { - let rates = await axios.get('https://api.opennode.co/v1/rates') - this.exchangeRates = rates.data.data - } catch (error) { - LNbits.utils.notifyApiError(error) - } - }, - navigateTo(page, opts = {stall: null, product: null, pubkey: null}) { - let {stall, product, pubkey} = opts - let url = new URL(window.location) - - if (pubkey) url.searchParams.set('merchant_pubkey', pubkey) - if (stall && !pubkey) { - pubkey = this.stalls.find(s => s.id == stall).pubkey - url.searchParams.set('merchant_pubkey', pubkey) - } - - switch (page) { - case 'stall': - if (stall) { - this.activeStall = stall - url.searchParams.set('stall_id', stall) - if (product) { - this.activeProduct = product - url.searchParams.set('product_id', product) - } - } - break - default: - this.activeStall = null - this.activeProduct = null - url.searchParams.delete('merchant_pubkey') - url.searchParams.delete('stall_id') - url.searchParams.delete('product_id') - break - } - - window.history.pushState({}, '', url) - this.activePage = page - }, - - getValueInSats(amount, unit = 'USD') { - if (!this.exchangeRates) return 0 - return Math.ceil( - (amount / this.exchangeRates[`BTC${unit}`][unit]) * 1e8 - ) - }, - getAmountFormated(amount, unit = 'USD') { - return LNbits.utils.formatCurrency(amount, unit) - }, - async addPubkey(pubkey = null) { - if (!pubkey) { - pubkey = String(this.inputPubkey).trim() - } - let regExp = /^#([0-9a-f]{3}){1,2}$/i - if (pubkey.startsWith('n')) { - try { - let {type, data} = nostr.nip19.decode(pubkey) - if (type === 'npub') pubkey = data - else if (type === 'nprofile') { - pubkey = data.pubkey - givenRelays = data.relays - } - this.pubkeys.add(pubkey) - this.inputPubkey = null - } catch (err) { - console.error(err) - } - } else if (regExp.test(pubkey)) { - pubkey = pubkey - } - this.pubkeys.add(pubkey) - this.$q.localStorage.set( - `diagonAlley.merchants`, - Array.from(this.pubkeys) - ) - await this.initNostr() - }, - removePubkey(pubkey) { - // Needs a hack for Vue reactivity - let pubkeys = this.pubkeys - pubkeys.delete(pubkey) - this.profiles.delete(pubkey) - this.pubkeys = new Set(Array.from(pubkeys)) - this.$q.localStorage.set( - `diagonAlley.merchants`, - Array.from(this.pubkeys) - ) - }, - async addRelay() { - let relay = String(this.inputRelay).trim() - if (!relay.startsWith('ws')) { - console.debug('invalid url') - return - } - this.relays.add(relay) - this.$q.localStorage.set(`diagonAlley.relays`, Array.from(this.relays)) - this.inputRelay = null - await this.initNostr() - }, - removeRelay(relay) { - // Needs a hack for Vue reactivity - let relays = this.relays - relays.delete(relay) - this.relays = new Set(Array.from(relays)) - this.$q.localStorage.set(`diagonAlley.relays`, Array.from(this.relays)) - } - } - }) - {% endblock %} From cb70b7775048ec468389c43ae59351e18e763ac6 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Fri, 3 Mar 2023 14:09:13 +0000 Subject: [PATCH 216/891] detach js from html --- .../customer-market/customer-market.html | 82 +---- .../customer-market/customer-market.js | 9 +- .../components/product-card/product-card.html | 76 +++++ .../components/product-card/product-card.js | 14 + static/js/market.js | 308 ++++++++++++++++++ templates/nostrmarket/market.html | 296 +---------------- 6 files changed, 404 insertions(+), 381 deletions(-) create mode 100644 static/components/product-card/product-card.html create mode 100644 static/components/product-card/product-card.js create mode 100644 static/js/market.js diff --git a/static/components/customer-market/customer-market.html b/static/components/customer-market/customer-market.html index 97a00b9..5de0b0d 100644 --- a/static/components/customer-market/customer-market.html +++ b/static/components/customer-market/customer-market.html @@ -10,87 +10,7 @@ v-for="(item, idx) in products" :key="idx" > - - - - -
-
- {{ item.name }} -
-
- - -
- - -
-
- {{ item.stallName }} -
- - {{ item.price }} satsBTC {{ (item.price / 1e8).toFixed(8) }} - - - {{ item.formatedPrice }} - ({{ item.priceInSats }} sats) - - {{ item.amount }} left -
-
- {{cat}} -
-
-

{{ item.description }}

-
-
- - - - - Stall: {{ item.stallName }} -
- - View details - - - Visit Stall - -
-
-
+
diff --git a/static/components/customer-market/customer-market.js b/static/components/customer-market/customer-market.js index 5844105..992222a 100644 --- a/static/components/customer-market/customer-market.js +++ b/static/components/customer-market/customer-market.js @@ -4,14 +4,11 @@ async function customerMarket(path) { name: 'customer-market', template, - props: ['products', 'exchange-rates'], + props: ['products', 'exchange-rates', 'change-page'], data: function () { return {} }, - methods: { - changePage() { - return - } - } + methods: {}, + created() {} }) } diff --git a/static/components/product-card/product-card.html b/static/components/product-card/product-card.html new file mode 100644 index 0000000..5ff2782 --- /dev/null +++ b/static/components/product-card/product-card.html @@ -0,0 +1,76 @@ + + + + +
+
{{ product.name }}
+
+ + +
+ + +
+
{{ product.stallName }}
+ + {{ product.price }} satsBTC {{ (product.price / 1e8).toFixed(8) }} + + + {{ product.formatedPrice }} + ({{ product.priceInSats }} sats) + + {{ product.quantity }} left +
+
+ {{cat}} +
+
+

{{ product.description }}

+
+
+ + + + + Stall: {{ product.stallName }} +
+ + View details + + + Visit Stall + +
+
+
diff --git a/static/components/product-card/product-card.js b/static/components/product-card/product-card.js new file mode 100644 index 0000000..da3fca3 --- /dev/null +++ b/static/components/product-card/product-card.js @@ -0,0 +1,14 @@ +async function productCard(path) { + const template = await loadTemplateAsync(path) + Vue.component('product-card', { + name: 'product-card', + template, + + props: ['product'], + data: function () { + return {} + }, + methods: {}, + created() {} + }) +} diff --git a/static/js/market.js b/static/js/market.js new file mode 100644 index 0000000..3c823fc --- /dev/null +++ b/static/js/market.js @@ -0,0 +1,308 @@ +const market = async () => { + Vue.component(VueQrcode.name, VueQrcode) + + const nostr = window.NostrTools + const defaultRelays = [ + 'wss://relay.damus.io', + 'wss://relay.snort.social', + 'wss://nos.lol', + 'wss://nostr.wine', + 'wss://relay.nostr.bg', + 'wss://nostr-pub.wellorder.net', + 'wss://nostr-pub.semisol.dev', + 'wss://eden.nostr.land', + 'wss://nostr.mom', + 'wss://nostr.fmt.wiz.biz', + 'wss://nostr.zebedee.cloud', + 'wss://nostr.rocks' + ] + const eventToObj = event => { + event.content = JSON.parse(event.content) + return { + ...event, + ...Object.values(event.tags).reduce((acc, tag) => { + let [key, value] = tag + return {...acc, [key]: [...(acc[key] || []), value]} + }, {}) + } + } + await Promise.all([ + productCard('static/components/product-card/product-card.html'), + customerMarket('static/components/customer-market/customer-market.html'), + customerStall('static/components/customer-stall/customer-stall.html'), + productDetail('static/components/product-detail/product-detail.html') + ]) + + new Vue({ + el: '#vue', + mixins: [windowMixin], + data: function () { + return { + drawer: false, + pubkeys: new Set(), + relays: new Set(), + events: [], + stalls: [], + products: [], + profiles: new Map(), + searchText: null, + exchangeRates: null, + inputPubkey: null, + inputRelay: null, + activePage: 'market', + activeStall: null, + activeProduct: null + } + }, + computed: { + filterProducts() { + let products = this.products + if (this.activeStall) { + products = products.filter(p => p.stall == this.activeStall) + } + if (!this.searchText || this.searchText.length < 2) return products + return products.filter(p => { + return ( + p.name.includes(this.searchText) || + p.description.includes(this.searchText) || + p.categories.includes(this.searchText) + ) + }) + }, + stallName() { + return this.stalls.find(s => s.id == this.activeStall)?.name || 'Stall' + }, + productName() { + return ( + this.products.find(p => p.id == this.activeProduct)?.name || 'Product' + ) + } + }, + async created() { + // Check for stored merchants and relays on localStorage + try { + let merchants = this.$q.localStorage.getItem(`diagonAlley.merchants`) + let relays = this.$q.localStorage.getItem(`diagonAlley.relays`) + if (merchants && merchants.length) { + this.pubkeys = new Set(merchants) + } + if (relays && relays.length) { + this.relays = new Set([...defaultRelays, ...relays]) + } + } catch (e) { + console.error(e) + } + // Hardcode pubkeys for testing + /* + this.pubkeys.add( + 'c1415f950a1e3431de2bc5ee35144639e2f514cf158279abff9ed77d50118796' + ) + this.pubkeys.add( + '8f69ac99b96f7c4ad58b98cc38fe5d35ce02daefae7d1609c797ce3b4f92f5fd' + ) + */ + // stall ids S4hQgtTwiF5kGJZPbqMH9M jkCbdtkXeMjGBY3LBf8yn4 + /*let naddr = nostr.nip19.naddrEncode({ + identifier: '1234', + pubkey: + 'c1415f950a1e3431de2bc5ee35144639e2f514cf158279abff9ed77d50118796', + kind: 30018, + relays: defaultRelays + }) + console.log(naddr) + console.log(nostr.nip19.decode(naddr)) + */ + let params = new URLSearchParams(window.location.search) + let merchant_pubkey = params.get('merchant_pubkey') + let stall_id = params.get('stall_id') + let product_id = params.get('product_id') + console.log(merchant_pubkey, stall_id, product_id) + if (merchant_pubkey) { + await addPubkey(merchant_pubkey) + /*LNbits.utils + .confirmDialog( + `We found a merchant pubkey in your request. Do you want to add it to the merchants list?` + ) + .onCancel(() => {}) + .onDismiss(() => {}) + .onOk(() => { + this.pubkeys.add(merchant_pubkey) + })*/ + } + this.$q.loading.show() + this.relays = new Set(defaultRelays) + // Get notes from Nostr + await this.initNostr() + + // What component to render on start + if (stall_id) { + if (product_id) { + this.activeProduct = product_id + } + this.activePage = 'stall' + this.activeStall = stall_id + } + + this.$q.loading.hide() + }, + methods: { + async initNostr() { + const pool = new nostr.SimplePool() + let relays = Array.from(this.relays) + let products = new Map() + let stalls = new Map() + // Get metadata and market data from the pubkeys + let sub = await pool + .list(relays, [ + { + kinds: [0, 30017, 30018], // for production kind is 30017 + authors: Array.from(this.pubkeys) + } + ]) + .then(events => { + console.log(events) + this.events = events || [] + this.events.map(eventToObj).map(e => { + if (e.kind == 0) { + this.profiles.set(e.pubkey, e.content) + return + } else if (e.kind == 30018) { + //it's a product `d` is the prod. id + products.set(e.d, {...e.content, id: e.d, categories: e.t}) + } else if (e.kind == 30017) { + // it's a stall `d` is the stall id + stalls.set(e.d, {...e.content, id: e.d, pubkey: e.pubkey}) + return + } + }) + }) + await Promise.resolve(sub) + this.stalls = await Array.from(stalls.values()) + + this.products = Array.from(products.values()).map(obj => { + let stall = this.stalls.find(s => s.id == obj.stall_id) + obj.stallName = stall.name + if (obj.currency != 'sat') { + obj.formatedPrice = this.getAmountFormated(obj.price, obj.currency) + obj.priceInSats = this.getValueInSats(obj.price, obj.currency) + } + return obj + }) + + pool.close(relays) + }, + async getRates() { + let noFiat = this.stalls.map(s => s.currency).every(c => c == 'sat') + if (noFiat) return + try { + let rates = await axios.get('https://api.opennode.co/v1/rates') + this.exchangeRates = rates.data.data + } catch (error) { + LNbits.utils.notifyApiError(error) + } + }, + navigateTo(page, opts = {stall: null, product: null, pubkey: null}) { + let {stall, product, pubkey} = opts + let url = new URL(window.location) + + if (pubkey) url.searchParams.set('merchant_pubkey', pubkey) + if (stall && !pubkey) { + pubkey = this.stalls.find(s => s.id == stall).pubkey + url.searchParams.set('merchant_pubkey', pubkey) + } + + switch (page) { + case 'stall': + if (stall) { + this.activeStall = stall + url.searchParams.set('stall_id', stall) + if (product) { + this.activeProduct = product + url.searchParams.set('product_id', product) + } + } + break + default: + this.activeStall = null + this.activeProduct = null + url.searchParams.delete('merchant_pubkey') + url.searchParams.delete('stall_id') + url.searchParams.delete('product_id') + break + } + + window.history.pushState({}, '', url) + this.activePage = page + }, + + getValueInSats(amount, unit = 'USD') { + if (!this.exchangeRates) return 0 + return Math.ceil( + (amount / this.exchangeRates[`BTC${unit}`][unit]) * 1e8 + ) + }, + getAmountFormated(amount, unit = 'USD') { + return LNbits.utils.formatCurrency(amount, unit) + }, + async addPubkey(pubkey = null) { + if (!pubkey) { + pubkey = String(this.inputPubkey).trim() + } + let regExp = /^#([0-9a-f]{3}){1,2}$/i + if (pubkey.startsWith('n')) { + try { + let {type, data} = nostr.nip19.decode(pubkey) + if (type === 'npub') pubkey = data + else if (type === 'nprofile') { + pubkey = data.pubkey + givenRelays = data.relays + } + this.pubkeys.add(pubkey) + this.inputPubkey = null + } catch (err) { + console.error(err) + } + } else if (regExp.test(pubkey)) { + pubkey = pubkey + } + this.pubkeys.add(pubkey) + this.$q.localStorage.set( + `diagonAlley.merchants`, + Array.from(this.pubkeys) + ) + await this.initNostr() + }, + removePubkey(pubkey) { + // Needs a hack for Vue reactivity + let pubkeys = this.pubkeys + pubkeys.delete(pubkey) + this.profiles.delete(pubkey) + this.pubkeys = new Set(Array.from(pubkeys)) + this.$q.localStorage.set( + `diagonAlley.merchants`, + Array.from(this.pubkeys) + ) + }, + async addRelay() { + let relay = String(this.inputRelay).trim() + if (!relay.startsWith('ws')) { + console.debug('invalid url') + return + } + this.relays.add(relay) + this.$q.localStorage.set(`diagonAlley.relays`, Array.from(this.relays)) + this.inputRelay = null + await this.initNostr() + }, + removeRelay(relay) { + // Needs a hack for Vue reactivity + let relays = this.relays + relays.delete(relay) + this.relays = new Set(Array.from(relays)) + this.$q.localStorage.set(`diagonAlley.relays`, Array.from(this.relays)) + } + } + }) +} + +market() diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html index 8469175..0d3c36e 100644 --- a/templates/nostrmarket/market.html +++ b/templates/nostrmarket/market.html @@ -139,7 +139,6 @@
- + - - Vue.component(VueQrcode.name, VueQrcode) - - Promise.all([ - customerMarket('static/components/customer-market/customer-market.html'), - customerStall('static/components/customer-stall/customer-stall.html'), - productDetail('static/components/product-detail/product-detail.html') - ]) - - new Vue({ - el: '#vue', - mixins: [windowMixin], - data: function () { - return { - drawer: false, - pubkeys: new Set(), - relays: new Set(), - events: [], - stalls: [], - products: [], - profiles: new Map(), - searchText: null, - exchangeRates: null, - inputPubkey: null, - inputRelay: null, - activePage: 'market', - activeStall: null, - activeProduct: null - } - }, - computed: { - filterProducts() { - let products = this.products - if (this.activeStall) { - products = products.filter(p => p.stall == this.activeStall) - } - if (!this.searchText || this.searchText.length < 2) return products - return products.filter(p => { - return ( - p.name.includes(this.searchText) || - p.description.includes(this.searchText) || - p.categories.includes(this.searchText) - ) - }) - }, - stallName() { - return this.stalls.find(s => s.id == this.activeStall)?.name || 'Stall' - }, - productName() { - return ( - this.products.find(p => p.id == this.activeProduct)?.name || 'Product' - ) - } - }, - async created() { - // Check for stored merchants and relays on localStorage - try { - let merchants = this.$q.localStorage.getItem(`diagonAlley.merchants`) - let relays = this.$q.localStorage.getItem(`diagonAlley.relays`) - if (merchants && merchants.length) { - this.pubkeys = new Set(merchants) - } - if (relays && relays.length) { - this.relays = new Set([...defaultRelays, ...relays]) - } - } catch (e) { - console.error(e) - } - // Hardcode pubkeys for testing - /* - this.pubkeys.add( - 'c1415f950a1e3431de2bc5ee35144639e2f514cf158279abff9ed77d50118796' - ) - this.pubkeys.add( - '8f69ac99b96f7c4ad58b98cc38fe5d35ce02daefae7d1609c797ce3b4f92f5fd' - ) - */ - // stall ids S4hQgtTwiF5kGJZPbqMH9M jkCbdtkXeMjGBY3LBf8yn4 - let merchant_pubkey = JSON.parse('{{ merchant_pubkey | tojson }}') - let stall_id = JSON.parse('{{ stall_id | tojson }}') - let product_id = JSON.parse('{{ product_id | tojson }}') - if (merchant_pubkey) { - await addPubkey(merchant_pubkey) - /*LNbits.utils - .confirmDialog( - `We found a merchant pubkey in your request. Do you want to add it to the merchants list?` - ) - .onCancel(() => {}) - .onDismiss(() => {}) - .onOk(() => { - this.pubkeys.add(merchant_pubkey) - })*/ - } - this.$q.loading.show() - this.relays = new Set(defaultRelays) - // Get notes from Nostr - await this.initNostr() - - // What component to render on start - if (stall_id) { - if (product_id) { - this.activeProduct = product_id - } - this.activePage = 'stall' - this.activeStall = stall_id - } - - this.$q.loading.hide() - }, - methods: { - async initNostr() { - const pool = new nostr.SimplePool() - let relays = Array.from(this.relays) - let products = new Map() - let stalls = new Map() - // Get metadata and market data from the pubkeys - let sub = await pool - .list(relays, [ - { - kinds: [0, 30017, 30018], // for production kind is 30017 - authors: Array.from(this.pubkeys) - } - ]) - .then(events => { - console.log(events) - this.events = events || [] - this.events.map(eventToObj).map(e => { - if (e.kind == 0) { - this.profiles.set(e.pubkey, e.content) - return - } else if (e.kind == 30018) { - //it's a product `d` is the prod. id - products.set(e.d, {...e.content, id: e.d, categories: e.t}) - } else if (e.kind == 30017) { - // it's a stall `d` is the stall id - stalls.set(e.d, {...e.content, id: e.d, pubkey: e.pubkey}) - return - } - }) - }) - await Promise.resolve(sub) - this.stalls = await Array.from(stalls.values()) - - this.products = Array.from(products.values()).map(obj => { - let stall = this.stalls.find(s => s.id == obj.stall_id) - obj.stallName = stall.name - if (obj.currency != 'sat') { - obj.formatedPrice = this.getAmountFormated(obj.price, obj.currency) - obj.priceInSats = this.getValueInSats(obj.price, obj.currency) - } - return obj - }) - - pool.close(relays) - }, - async getRates() { - let noFiat = this.stalls.map(s => s.currency).every(c => c == 'sat') - if (noFiat) return - try { - let rates = await axios.get('https://api.opennode.co/v1/rates') - this.exchangeRates = rates.data.data - } catch (error) { - LNbits.utils.notifyApiError(error) - } - }, - navigateTo(page, opts = {stall: null, product: null, pubkey: null}) { - let {stall, product, pubkey} = opts - let url = new URL(window.location) - - if (pubkey) url.searchParams.set('merchant_pubkey', pubkey) - if (stall && !pubkey) { - pubkey = this.stalls.find(s => s.id == stall).pubkey - url.searchParams.set('merchant_pubkey', pubkey) - } - - switch (page) { - case 'stall': - if (stall) { - this.activeStall = stall - url.searchParams.set('stall_id', stall) - if (product) { - this.activeProduct = product - url.searchParams.set('product_id', product) - } - } - break - default: - this.activeStall = null - this.activeProduct = null - url.searchParams.delete('merchant_pubkey') - url.searchParams.delete('stall_id') - url.searchParams.delete('product_id') - break - } - - window.history.pushState({}, '', url) - this.activePage = page - }, - - getValueInSats(amount, unit = 'USD') { - if (!this.exchangeRates) return 0 - return Math.ceil( - (amount / this.exchangeRates[`BTC${unit}`][unit]) * 1e8 - ) - }, - getAmountFormated(amount, unit = 'USD') { - return LNbits.utils.formatCurrency(amount, unit) - }, - async addPubkey(pubkey = null) { - if (!pubkey) { - pubkey = String(this.inputPubkey).trim() - } - let regExp = /^#([0-9a-f]{3}){1,2}$/i - if (pubkey.startsWith('n')) { - try { - let {type, data} = nostr.nip19.decode(pubkey) - if (type === 'npub') pubkey = data - else if (type === 'nprofile') { - pubkey = data.pubkey - givenRelays = data.relays - } - this.pubkeys.add(pubkey) - this.inputPubkey = null - } catch (err) { - console.error(err) - } - } else if (regExp.test(pubkey)) { - pubkey = pubkey - } - this.pubkeys.add(pubkey) - this.$q.localStorage.set( - `diagonAlley.merchants`, - Array.from(this.pubkeys) - ) - await this.initNostr() - }, - removePubkey(pubkey) { - // Needs a hack for Vue reactivity - let pubkeys = this.pubkeys - pubkeys.delete(pubkey) - this.profiles.delete(pubkey) - this.pubkeys = new Set(Array.from(pubkeys)) - this.$q.localStorage.set( - `diagonAlley.merchants`, - Array.from(this.pubkeys) - ) - }, - async addRelay() { - let relay = String(this.inputRelay).trim() - if (!relay.startsWith('ws')) { - console.debug('invalid url') - return - } - this.relays.add(relay) - this.$q.localStorage.set(`diagonAlley.relays`, Array.from(this.relays)) - this.inputRelay = null - await this.initNostr() - }, - removeRelay(relay) { - // Needs a hack for Vue reactivity - let relays = this.relays - relays.delete(relay) - this.relays = new Set(Array.from(relays)) - this.$q.localStorage.set(`diagonAlley.relays`, Array.from(this.relays)) - } - } - }) - {% endblock %} From 64ed5795b8a3e887509ace3b8f5a4deda88709c9 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Fri, 3 Mar 2023 16:53:35 +0000 Subject: [PATCH 217/891] routing done --- .../customer-market/customer-market.html | 2 +- .../customer-market/customer-market.js | 6 +- .../customer-stall/customer-stall.html | 93 ++------------ .../customer-stall/customer-stall.js | 44 ++++--- .../components/product-card/product-card.html | 1 + .../components/product-card/product-card.js | 2 +- .../product-detail/product-detail.html | 118 +----------------- static/js/market.js | 76 +++++------ templates/nostrmarket/market.html | 3 +- 9 files changed, 78 insertions(+), 267 deletions(-) diff --git a/static/components/customer-market/customer-market.html b/static/components/customer-market/customer-market.html index 5de0b0d..ce52d87 100644 --- a/static/components/customer-market/customer-market.html +++ b/static/components/customer-market/customer-market.html @@ -10,7 +10,7 @@ v-for="(item, idx) in products" :key="idx" > - +
diff --git a/static/components/customer-market/customer-market.js b/static/components/customer-market/customer-market.js index 992222a..c14ffa9 100644 --- a/static/components/customer-market/customer-market.js +++ b/static/components/customer-market/customer-market.js @@ -8,7 +8,11 @@ async function customerMarket(path) { data: function () { return {} }, - methods: {}, + methods: { + changePageM(page, opts) { + this.$emit('change-page', page, opts) + } + }, created() {} }) } diff --git a/static/components/customer-stall/customer-stall.html b/static/components/customer-stall/customer-stall.html index 306a5f7..9bc302c 100644 --- a/static/components/customer-stall/customer-stall.html +++ b/static/components/customer-stall/customer-stall.html @@ -10,94 +10,25 @@ - + - +
+ +
+ +
+
- - - - - Add to cart -
-
- {{ item.name }} -
-
- - -
- - -
-
- {{ item.stallName }} -
- - {{ item.price }} satsBTC {{ (item.price / 1e8).toFixed(8) }} - - - {{ item.formatedPrice }} - ({{ item.priceInSats }} sats) - - {{ item.amount }} left -
-
- {{cat}} -
-
-

{{ item.description }}

-
-
- - - - - Stall: {{ item.stallName }} -
- See product -
-
-
+
diff --git a/static/components/customer-stall/customer-stall.js b/static/components/customer-stall/customer-stall.js index 7606d68..1d9b395 100644 --- a/static/components/customer-stall/customer-stall.js +++ b/static/components/customer-stall/customer-stall.js @@ -1,34 +1,32 @@ async function customerStall(path) { const template = await loadTemplateAsync(path) - const mock = { - stall: '4M8j9KKGzUckHgb4C3pKCv', - name: 'product 1', - description: - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Leo integer malesuada nunc vel risus commodo. Sapien faucibus et molestie ac feugiat sed lectus vestibulum mattis. Cras ornare arcu dui vivamus. Risus pretium quam vulputate dignissim suspendisse in est ante in. Faucibus in ornare quam viverra orci sagittis eu volutpat odio.', - amount: 100, - price: '10', - images: ['https://i.imgur.com/cEfpEjq.jpeg'], - id: ['RyMbE9Hdwk9X333JKtkkNS'], - categories: ['crafts', 'robots'], - currency: 'EUR', - stallName: 'stall 1', - formatedPrice: '€10.00', - priceInSats: 0 - } + Vue.component('customer-stall', { name: 'customer-stall', template, - props: ['stall', 'products', 'exchange-rates', 'product-detail'], + props: [ + 'stall', + 'products', + 'exchange-rates', + 'product-detail', + 'change-page' + ], data: function () { - return { - mock: mock + return {} + }, + computed: { + product() { + if (this.productDetail) { + return this.products.find(p => p.id == this.productDetail) + } } }, - methods: {}, - created() { - console.log(this.stall) - console.log(this.products) - } + methods: { + changePageS(page, opts) { + this.$emit('change-page', page, opts) + } + }, + created() {} }) } diff --git a/static/components/product-card/product-card.html b/static/components/product-card/product-card.html index 5ff2782..93d4d96 100644 --- a/static/components/product-card/product-card.html +++ b/static/components/product-card/product-card.html @@ -52,6 +52,7 @@ Stall: {{ product.stallName }} + {{ $parent.activeStall }}
{{ product.amount > 0 ? 'In stock.' : 'Out of stock.' }} -
-
-
Customer rating
-
4.2
-
- -
-
(357 reviews)
-
- 93% would recommend to a friend -
- - - - 5 - - - 273 - - - 4 - - -   69 - - - 3 - - -      6 - - - 2 - - -      3 - - - 1 - - -      6 - - -
-
- -
diff --git a/static/js/market.js b/static/js/market.js index 3c823fc..6a0d723 100644 --- a/static/js/market.js +++ b/static/js/market.js @@ -30,7 +30,8 @@ const market = async () => { productCard('static/components/product-card/product-card.html'), customerMarket('static/components/customer-market/customer-market.html'), customerStall('static/components/customer-stall/customer-stall.html'), - productDetail('static/components/product-detail/product-detail.html') + productDetail('static/components/product-detail/product-detail.html'), + shoppingCart('static/components/shopping-cart/shopping-cart.html') ]) new Vue({ @@ -58,7 +59,7 @@ const market = async () => { filterProducts() { let products = this.products if (this.activeStall) { - products = products.filter(p => p.stall == this.activeStall) + products = products.filter(p => p.stall_id == this.activeStall) } if (!this.searchText || this.searchText.length < 2) return products return products.filter(p => { @@ -76,6 +77,9 @@ const market = async () => { return ( this.products.find(p => p.id == this.activeProduct)?.name || 'Product' ) + }, + isLoading() { + return this.$q.loading.isActive } }, async created() { @@ -88,51 +92,17 @@ const market = async () => { } if (relays && relays.length) { this.relays = new Set([...defaultRelays, ...relays]) + } else { + this.relays = new Set(defaultRelays) } } catch (e) { console.error(e) } - // Hardcode pubkeys for testing - /* - this.pubkeys.add( - 'c1415f950a1e3431de2bc5ee35144639e2f514cf158279abff9ed77d50118796' - ) - this.pubkeys.add( - '8f69ac99b96f7c4ad58b98cc38fe5d35ce02daefae7d1609c797ce3b4f92f5fd' - ) - */ - // stall ids S4hQgtTwiF5kGJZPbqMH9M jkCbdtkXeMjGBY3LBf8yn4 - /*let naddr = nostr.nip19.naddrEncode({ - identifier: '1234', - pubkey: - 'c1415f950a1e3431de2bc5ee35144639e2f514cf158279abff9ed77d50118796', - kind: 30018, - relays: defaultRelays - }) - console.log(naddr) - console.log(nostr.nip19.decode(naddr)) - */ + let params = new URLSearchParams(window.location.search) let merchant_pubkey = params.get('merchant_pubkey') let stall_id = params.get('stall_id') let product_id = params.get('product_id') - console.log(merchant_pubkey, stall_id, product_id) - if (merchant_pubkey) { - await addPubkey(merchant_pubkey) - /*LNbits.utils - .confirmDialog( - `We found a merchant pubkey in your request. Do you want to add it to the merchants list?` - ) - .onCancel(() => {}) - .onDismiss(() => {}) - .onOk(() => { - this.pubkeys.add(merchant_pubkey) - })*/ - } - this.$q.loading.show() - this.relays = new Set(defaultRelays) - // Get notes from Nostr - await this.initNostr() // What component to render on start if (stall_id) { @@ -142,11 +112,33 @@ const market = async () => { this.activePage = 'stall' this.activeStall = stall_id } + if (merchant_pubkey && !this.pubkeys.has(merchant_pubkey)) { + await LNbits.utils + .confirmDialog( + `We found a merchant pubkey in your request. Do you want to add it to the merchants list?` + ) + .onOk(async () => { + await this.addPubkey(merchant_pubkey) + }) + } + // Get notes from Nostr + await this.initNostr() this.$q.loading.hide() }, methods: { + naddr() { + let naddr = nostr.nip19.naddrEncode({ + identifier: '1234', + pubkey: + 'c1415f950a1e3431de2bc5ee35144639e2f514cf158279abff9ed77d50118796', + kind: 30018, + relays: defaultRelays + }) + console.log(naddr) + }, async initNostr() { + this.$q.loading.show() const pool = new nostr.SimplePool() let relays = Array.from(this.relays) let products = new Map() @@ -168,10 +160,10 @@ const market = async () => { return } else if (e.kind == 30018) { //it's a product `d` is the prod. id - products.set(e.d, {...e.content, id: e.d, categories: e.t}) + products.set(e.d, {...e.content, id: e.d[0], categories: e.t}) } else if (e.kind == 30017) { // it's a stall `d` is the stall id - stalls.set(e.d, {...e.content, id: e.d, pubkey: e.pubkey}) + stalls.set(e.d, {...e.content, id: e.d[0], pubkey: e.pubkey}) return } }) @@ -182,13 +174,13 @@ const market = async () => { this.products = Array.from(products.values()).map(obj => { let stall = this.stalls.find(s => s.id == obj.stall_id) obj.stallName = stall.name + obj.images = [obj.image] if (obj.currency != 'sat') { obj.formatedPrice = this.getAmountFormated(obj.price, obj.currency) obj.priceInSats = this.getValueInSats(obj.price, obj.currency) } return obj }) - pool.close(relays) }, async getRates() { diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html index 0d3c36e..c885e10 100644 --- a/templates/nostrmarket/market.html +++ b/templates/nostrmarket/market.html @@ -140,7 +140,7 @@ + {% endblock %} From 85832b28c887e50ea20a840437e79c64ae14ef61 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Fri, 3 Mar 2023 16:53:35 +0000 Subject: [PATCH 218/891] routing done --- .../customer-market/customer-market.html | 2 +- .../customer-market/customer-market.js | 6 +- .../customer-stall/customer-stall.html | 93 ++------------ .../customer-stall/customer-stall.js | 44 ++++--- .../components/product-card/product-card.html | 1 + .../components/product-card/product-card.js | 2 +- .../product-detail/product-detail.html | 118 +----------------- static/js/market.js | 76 +++++------ templates/nostrmarket/market.html | 3 +- 9 files changed, 78 insertions(+), 267 deletions(-) diff --git a/static/components/customer-market/customer-market.html b/static/components/customer-market/customer-market.html index 5de0b0d..ce52d87 100644 --- a/static/components/customer-market/customer-market.html +++ b/static/components/customer-market/customer-market.html @@ -10,7 +10,7 @@ v-for="(item, idx) in products" :key="idx" > - + diff --git a/static/components/customer-market/customer-market.js b/static/components/customer-market/customer-market.js index 992222a..c14ffa9 100644 --- a/static/components/customer-market/customer-market.js +++ b/static/components/customer-market/customer-market.js @@ -8,7 +8,11 @@ async function customerMarket(path) { data: function () { return {} }, - methods: {}, + methods: { + changePageM(page, opts) { + this.$emit('change-page', page, opts) + } + }, created() {} }) } diff --git a/static/components/customer-stall/customer-stall.html b/static/components/customer-stall/customer-stall.html index 306a5f7..9bc302c 100644 --- a/static/components/customer-stall/customer-stall.html +++ b/static/components/customer-stall/customer-stall.html @@ -10,94 +10,25 @@ - + - +
+ +
+ +
+
- - - - - Add to cart -
-
- {{ item.name }} -
-
- - -
- - -
-
- {{ item.stallName }} -
- - {{ item.price }} satsBTC {{ (item.price / 1e8).toFixed(8) }} - - - {{ item.formatedPrice }} - ({{ item.priceInSats }} sats) - - {{ item.amount }} left -
-
- {{cat}} -
-
-

{{ item.description }}

-
-
- - - - - Stall: {{ item.stallName }} -
- See product -
-
-
+
diff --git a/static/components/customer-stall/customer-stall.js b/static/components/customer-stall/customer-stall.js index 7606d68..1d9b395 100644 --- a/static/components/customer-stall/customer-stall.js +++ b/static/components/customer-stall/customer-stall.js @@ -1,34 +1,32 @@ async function customerStall(path) { const template = await loadTemplateAsync(path) - const mock = { - stall: '4M8j9KKGzUckHgb4C3pKCv', - name: 'product 1', - description: - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Leo integer malesuada nunc vel risus commodo. Sapien faucibus et molestie ac feugiat sed lectus vestibulum mattis. Cras ornare arcu dui vivamus. Risus pretium quam vulputate dignissim suspendisse in est ante in. Faucibus in ornare quam viverra orci sagittis eu volutpat odio.', - amount: 100, - price: '10', - images: ['https://i.imgur.com/cEfpEjq.jpeg'], - id: ['RyMbE9Hdwk9X333JKtkkNS'], - categories: ['crafts', 'robots'], - currency: 'EUR', - stallName: 'stall 1', - formatedPrice: '€10.00', - priceInSats: 0 - } + Vue.component('customer-stall', { name: 'customer-stall', template, - props: ['stall', 'products', 'exchange-rates', 'product-detail'], + props: [ + 'stall', + 'products', + 'exchange-rates', + 'product-detail', + 'change-page' + ], data: function () { - return { - mock: mock + return {} + }, + computed: { + product() { + if (this.productDetail) { + return this.products.find(p => p.id == this.productDetail) + } } }, - methods: {}, - created() { - console.log(this.stall) - console.log(this.products) - } + methods: { + changePageS(page, opts) { + this.$emit('change-page', page, opts) + } + }, + created() {} }) } diff --git a/static/components/product-card/product-card.html b/static/components/product-card/product-card.html index 5ff2782..93d4d96 100644 --- a/static/components/product-card/product-card.html +++ b/static/components/product-card/product-card.html @@ -52,6 +52,7 @@ Stall: {{ product.stallName }} + {{ $parent.activeStall }}
{{ product.amount > 0 ? 'In stock.' : 'Out of stock.' }} -
-
-
Customer rating
-
4.2
-
- -
-
(357 reviews)
-
- 93% would recommend to a friend -
- - - - 5 - - - 273 - - - 4 - - -   69 - - - 3 - - -      6 - - - 2 - - -      3 - - - 1 - - -      6 - - -
-
- -
diff --git a/static/js/market.js b/static/js/market.js index 3c823fc..6a0d723 100644 --- a/static/js/market.js +++ b/static/js/market.js @@ -30,7 +30,8 @@ const market = async () => { productCard('static/components/product-card/product-card.html'), customerMarket('static/components/customer-market/customer-market.html'), customerStall('static/components/customer-stall/customer-stall.html'), - productDetail('static/components/product-detail/product-detail.html') + productDetail('static/components/product-detail/product-detail.html'), + shoppingCart('static/components/shopping-cart/shopping-cart.html') ]) new Vue({ @@ -58,7 +59,7 @@ const market = async () => { filterProducts() { let products = this.products if (this.activeStall) { - products = products.filter(p => p.stall == this.activeStall) + products = products.filter(p => p.stall_id == this.activeStall) } if (!this.searchText || this.searchText.length < 2) return products return products.filter(p => { @@ -76,6 +77,9 @@ const market = async () => { return ( this.products.find(p => p.id == this.activeProduct)?.name || 'Product' ) + }, + isLoading() { + return this.$q.loading.isActive } }, async created() { @@ -88,51 +92,17 @@ const market = async () => { } if (relays && relays.length) { this.relays = new Set([...defaultRelays, ...relays]) + } else { + this.relays = new Set(defaultRelays) } } catch (e) { console.error(e) } - // Hardcode pubkeys for testing - /* - this.pubkeys.add( - 'c1415f950a1e3431de2bc5ee35144639e2f514cf158279abff9ed77d50118796' - ) - this.pubkeys.add( - '8f69ac99b96f7c4ad58b98cc38fe5d35ce02daefae7d1609c797ce3b4f92f5fd' - ) - */ - // stall ids S4hQgtTwiF5kGJZPbqMH9M jkCbdtkXeMjGBY3LBf8yn4 - /*let naddr = nostr.nip19.naddrEncode({ - identifier: '1234', - pubkey: - 'c1415f950a1e3431de2bc5ee35144639e2f514cf158279abff9ed77d50118796', - kind: 30018, - relays: defaultRelays - }) - console.log(naddr) - console.log(nostr.nip19.decode(naddr)) - */ + let params = new URLSearchParams(window.location.search) let merchant_pubkey = params.get('merchant_pubkey') let stall_id = params.get('stall_id') let product_id = params.get('product_id') - console.log(merchant_pubkey, stall_id, product_id) - if (merchant_pubkey) { - await addPubkey(merchant_pubkey) - /*LNbits.utils - .confirmDialog( - `We found a merchant pubkey in your request. Do you want to add it to the merchants list?` - ) - .onCancel(() => {}) - .onDismiss(() => {}) - .onOk(() => { - this.pubkeys.add(merchant_pubkey) - })*/ - } - this.$q.loading.show() - this.relays = new Set(defaultRelays) - // Get notes from Nostr - await this.initNostr() // What component to render on start if (stall_id) { @@ -142,11 +112,33 @@ const market = async () => { this.activePage = 'stall' this.activeStall = stall_id } + if (merchant_pubkey && !this.pubkeys.has(merchant_pubkey)) { + await LNbits.utils + .confirmDialog( + `We found a merchant pubkey in your request. Do you want to add it to the merchants list?` + ) + .onOk(async () => { + await this.addPubkey(merchant_pubkey) + }) + } + // Get notes from Nostr + await this.initNostr() this.$q.loading.hide() }, methods: { + naddr() { + let naddr = nostr.nip19.naddrEncode({ + identifier: '1234', + pubkey: + 'c1415f950a1e3431de2bc5ee35144639e2f514cf158279abff9ed77d50118796', + kind: 30018, + relays: defaultRelays + }) + console.log(naddr) + }, async initNostr() { + this.$q.loading.show() const pool = new nostr.SimplePool() let relays = Array.from(this.relays) let products = new Map() @@ -168,10 +160,10 @@ const market = async () => { return } else if (e.kind == 30018) { //it's a product `d` is the prod. id - products.set(e.d, {...e.content, id: e.d, categories: e.t}) + products.set(e.d, {...e.content, id: e.d[0], categories: e.t}) } else if (e.kind == 30017) { // it's a stall `d` is the stall id - stalls.set(e.d, {...e.content, id: e.d, pubkey: e.pubkey}) + stalls.set(e.d, {...e.content, id: e.d[0], pubkey: e.pubkey}) return } }) @@ -182,13 +174,13 @@ const market = async () => { this.products = Array.from(products.values()).map(obj => { let stall = this.stalls.find(s => s.id == obj.stall_id) obj.stallName = stall.name + obj.images = [obj.image] if (obj.currency != 'sat') { obj.formatedPrice = this.getAmountFormated(obj.price, obj.currency) obj.priceInSats = this.getValueInSats(obj.price, obj.currency) } return obj }) - pool.close(relays) }, async getRates() { diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html index 0d3c36e..c885e10 100644 --- a/templates/nostrmarket/market.html +++ b/templates/nostrmarket/market.html @@ -140,7 +140,7 @@ + {% endblock %} From f71b9f56d9acbb76b0decd504dac82f6d81ba1f0 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Fri, 3 Mar 2023 16:53:46 +0000 Subject: [PATCH 219/891] init shopping cart --- .../components/shopping-cart/shopping-cart.html | 1 + static/components/shopping-cart/shopping-cart.js | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 static/components/shopping-cart/shopping-cart.html create mode 100644 static/components/shopping-cart/shopping-cart.js diff --git a/static/components/shopping-cart/shopping-cart.html b/static/components/shopping-cart/shopping-cart.html new file mode 100644 index 0000000..8650cc6 --- /dev/null +++ b/static/components/shopping-cart/shopping-cart.html @@ -0,0 +1 @@ + diff --git a/static/components/shopping-cart/shopping-cart.js b/static/components/shopping-cart/shopping-cart.js new file mode 100644 index 0000000..e0ad053 --- /dev/null +++ b/static/components/shopping-cart/shopping-cart.js @@ -0,0 +1,16 @@ +async function shoppingCart(path) { + const template = await loadTemplateAsync(path) + + Vue.component('shopping-cart', { + name: 'shopping-cart', + template, + + props: [], + data: function () { + return {} + }, + computed: {}, + methods: {}, + created() {} + }) +} From 562ac856534b9c4a8c3b2e463d867187afe1e00b Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Fri, 3 Mar 2023 16:53:46 +0000 Subject: [PATCH 220/891] init shopping cart --- .../components/shopping-cart/shopping-cart.html | 1 + static/components/shopping-cart/shopping-cart.js | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 static/components/shopping-cart/shopping-cart.html create mode 100644 static/components/shopping-cart/shopping-cart.js diff --git a/static/components/shopping-cart/shopping-cart.html b/static/components/shopping-cart/shopping-cart.html new file mode 100644 index 0000000..8650cc6 --- /dev/null +++ b/static/components/shopping-cart/shopping-cart.html @@ -0,0 +1 @@ + diff --git a/static/components/shopping-cart/shopping-cart.js b/static/components/shopping-cart/shopping-cart.js new file mode 100644 index 0000000..e0ad053 --- /dev/null +++ b/static/components/shopping-cart/shopping-cart.js @@ -0,0 +1,16 @@ +async function shoppingCart(path) { + const template = await loadTemplateAsync(path) + + Vue.component('shopping-cart', { + name: 'shopping-cart', + template, + + props: [], + data: function () { + return {} + }, + computed: {}, + methods: {}, + created() {} + }) +} From 52f8dead0ef5b4db59e2f8a8f435fc496b125e7e Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Sat, 4 Mar 2023 19:49:58 +0000 Subject: [PATCH 221/891] general fixes, prop passing and shopping cart (doesn't send dm) --- .../customer-stall/customer-stall.html | 106 ++++++++++- .../customer-stall/customer-stall.js | 167 +++++++++++++++++- .../components/product-card/product-card.html | 16 ++ .../components/product-card/product-card.js | 2 +- .../product-detail/product-detail.html | 12 +- .../product-detail/product-detail.js | 15 +- .../shopping-cart/shopping-cart.html | 52 +++++- .../components/shopping-cart/shopping-cart.js | 2 +- static/js/market.js | 17 +- templates/nostrmarket/market.html | 1 + templates/nostrmarket/stall.html | 61 ------- 11 files changed, 355 insertions(+), 96 deletions(-) delete mode 100644 templates/nostrmarket/stall.html diff --git a/static/components/customer-stall/customer-stall.html b/static/components/customer-stall/customer-stall.html index 9bc302c..aedec55 100644 --- a/static/components/customer-stall/customer-stall.html +++ b/static/components/customer-stall/customer-stall.html @@ -10,13 +10,20 @@ - +
@@ -28,7 +35,102 @@ v-for="(item, idx) in products" :key="idx" > - +
+ + + + + + + + + +
+
+ Generate key pair +
+
+ Get from Extension +
+
+ + +

Select the shipping zone:

+
+ +
+
+ Total: {{ stall.currency != 'sat' ? getAmountFormated(finalCost) : + finalCost + 'sats' }} + ({{ getValueInSats(finalCost) }} sats) +
+
+ Checkout + Cancel +
+
+
+
+ diff --git a/static/components/customer-stall/customer-stall.js b/static/components/customer-stall/customer-stall.js index 1d9b395..858ed9d 100644 --- a/static/components/customer-stall/customer-stall.js +++ b/static/components/customer-stall/customer-stall.js @@ -10,23 +10,184 @@ async function customerStall(path) { 'products', 'exchange-rates', 'product-detail', - 'change-page' + 'change-page', + 'relays' ], data: function () { - return {} + return { + cart: { + total: 0, + size: 0, + products: new Map() + }, + cartMenu: [], + hasNip07: false, + checkoutDialog: { + show: false, + data: { + pubkey: null + } + }, + qrCodeDialog: { + data: { + payment_request: null + }, + show: false + } + } }, computed: { product() { if (this.productDetail) { return this.products.find(p => p.id == this.productDetail) } + }, + finalCost() { + if (!this.checkoutDialog.data.shippingzone) return this.cart.total + + let zoneCost = this.stall.shipping.find( + z => z.id == this.checkoutDialog.data.shippingzone + ) + return +this.cart.total + zoneCost.cost } }, methods: { changePageS(page, opts) { this.$emit('change-page', page, opts) + }, + getValueInSats(amount, unit = 'USD') { + if (!this.exchangeRates) return 0 + return Math.ceil( + (amount / this.exchangeRates[`BTC${unit}`][unit]) * 1e8 + ) + }, + getAmountFormated(amount, unit = 'USD') { + return LNbits.utils.formatCurrency(amount, unit) + }, + addToCart(item) { + console.log('add to cart', item) + let prod = this.cart.products + if (prod.has(item.id)) { + let qty = prod.get(item.id).quantity + prod.set(item.id, { + ...prod.get(item.id), + quantity: qty + 1 + }) + } else { + prod.set(item.id, { + name: item.name, + quantity: 1, + price: item.price, + image: item?.images[0] || null + }) + } + this.$q.notify({ + type: 'positive', + message: `${item.name} added to cart`, + icon: 'thumb_up' + }) + this.cart.products = prod + this.updateCart(+item.price) + }, + removeFromCart(item) { + this.cart.products.delete(item.id) + this.updateCart(+item.price, true) + }, + updateCart(price, del = false) { + console.log(this.cart, this.cartMenu) + if (del) { + this.cart.total -= price + this.cart.size-- + } else { + this.cart.total += price + this.cart.size++ + } + this.cartMenu = Array.from(this.cart.products, item => { + return {id: item[0], ...item[1]} + }) + console.log(this.cart, this.cartMenu) + }, + resetCart() { + this.cart = { + total: 0, + size: 0, + products: new Map() + } + }, + async getPubkey() { + try { + this.checkoutDialog.data.pubkey = await window.nostr.getPublicKey() + this.checkoutDialog.data.privkey = null + } catch (err) { + console.error( + `Failed to get a public key from a Nostr extension: ${err}` + ) + } + }, + generateKeyPair() { + let sk = NostrTools.generatePrivateKey() + let pk = NostrTools.getPublicKey(sk) + this.checkoutDialog.data.pubkey = pk + this.checkoutDialog.data.privkey = sk + }, + placeOrder() { + LNbits.utils + .confirmDialog( + `Send the order to the merchant? You should receive a message with the payment details.` + ) + .onOk(async () => { + let orderData = this.checkoutDialog.data + let content = { + name: orderData?.username, + description: null, + address: orderData.address, + message: null, + contact: { + nostr: orderData.pubkey, + phone: null, + email: orderData?.email + }, + items: Array.from(this.cart.products, p => { + return {product_id: p[0], quantity: p[1].quantity} + }) + } + let event = { + kind: 4, + created_at: Math.floor(Date.now() / 1000), + tags: [], + content: await window.nostr.nip04.encrypt( + orderData.pubkey, + content + ), + pubkey: orderData.pubkey + } + event.id = NostrTools.getEventHash(event) + if (orderData.privkey) { + event.sig = NostrTools.signEvent(event, orderData.privkey) + } else if (this.hasNip07) { + await window.nostr.signEvent(event) + } + await this.sendOrder(event) + }) + }, + async sendOrder(order) { + const pool = new NostrTools.SimplePool() + let relays = Array.from(this.relays) + let pubs = await pool.publish(relays, order) + pubs.on('ok', relay => { + console.log(`${relay.url} has accepted our event`) + }) + pubs.on('failed', reason => { + console.log(`failed to publish to ${reason}`) + }) } }, - created() {} + created() { + setTimeout(() => { + if (window.nostr) { + this.hasNip07 = true + } + }, 1000) + } }) } diff --git a/static/components/product-card/product-card.html b/static/components/product-card/product-card.html index 93d4d96..5a92ba3 100644 --- a/static/components/product-card/product-card.html +++ b/static/components/product-card/product-card.html @@ -9,6 +9,22 @@ > + Add to cart
{{ product.name }}
diff --git a/static/components/product-card/product-card.js b/static/components/product-card/product-card.js index 9e8490b..5e049df 100644 --- a/static/components/product-card/product-card.js +++ b/static/components/product-card/product-card.js @@ -4,7 +4,7 @@ async function productCard(path) { name: 'product-card', template, - props: ['product', 'change-page'], + props: ['product', 'change-page', 'add-to-cart', 'is-stall'], data: function () { return {} }, diff --git a/static/components/product-detail/product-detail.html b/static/components/product-detail/product-detail.html index 92fa1c0..be31eda 100644 --- a/static/components/product-detail/product-detail.html +++ b/static/components/product-detail/product-detail.html @@ -17,6 +17,11 @@ style="/*background-size: contain; background-repeat: no-repeat*/" > +
@@ -47,7 +52,7 @@ {{ product.amount > 0 ? 'In stock.' : 'Out of stock.' }}{{ product.quantity > 0 ? 'In stock.' : 'Out of stock.' }}
@@ -56,12 +61,13 @@ color="primary" icon="shopping_cart" label="Add to cart" + @click="$emit('add-to-cart', product)" />
diff --git a/static/components/product-detail/product-detail.js b/static/components/product-detail/product-detail.js index 7b60f6b..d55b653 100644 --- a/static/components/product-detail/product-detail.js +++ b/static/components/product-detail/product-detail.js @@ -4,23 +4,14 @@ async function productDetail(path) { name: 'product-detail', template, - props: ['product'], + props: ['product', 'add-to-cart'], data: function () { return { slide: 1 } }, - computed: { - win_width() { - return this.$q.screen.width - 59 - }, - win_height() { - return this.$q.screen.height - 0 - } - }, + computed: {}, methods: {}, - created() { - console.log('ping') - } + created() {} }) } diff --git a/static/components/shopping-cart/shopping-cart.html b/static/components/shopping-cart/shopping-cart.html index 8650cc6..2864cf3 100644 --- a/static/components/shopping-cart/shopping-cart.html +++ b/static/components/shopping-cart/shopping-cart.html @@ -1 +1,51 @@ - + + + {{ cart.size }} + + + + + + {{p.quantity}} x + + + + + + + + + {{ p.name }} + + + + + {{p.currency != 'sat' ? p.formatedPrice : p.price + 'sats'}} + + + + + + +
+ +
diff --git a/static/components/shopping-cart/shopping-cart.js b/static/components/shopping-cart/shopping-cart.js index e0ad053..8f6902d 100644 --- a/static/components/shopping-cart/shopping-cart.js +++ b/static/components/shopping-cart/shopping-cart.js @@ -5,7 +5,7 @@ async function shoppingCart(path) { name: 'shopping-cart', template, - props: [], + props: ['cart', 'cart-menu', 'remove-from-cart', 'reset-cart'], data: function () { return {} }, diff --git a/static/js/market.js b/static/js/market.js index 6a0d723..95bf5a6 100644 --- a/static/js/market.js +++ b/static/js/market.js @@ -1,20 +1,13 @@ const market = async () => { Vue.component(VueQrcode.name, VueQrcode) - const nostr = window.NostrTools + const NostrTools = window.NostrTools const defaultRelays = [ 'wss://relay.damus.io', 'wss://relay.snort.social', - 'wss://nos.lol', 'wss://nostr.wine', - 'wss://relay.nostr.bg', 'wss://nostr-pub.wellorder.net', - 'wss://nostr-pub.semisol.dev', - 'wss://eden.nostr.land', - 'wss://nostr.mom', - 'wss://nostr.fmt.wiz.biz', - 'wss://nostr.zebedee.cloud', - 'wss://nostr.rocks' + 'wss://nostr.zebedee.cloud' ] const eventToObj = event => { event.content = JSON.parse(event.content) @@ -128,7 +121,7 @@ const market = async () => { }, methods: { naddr() { - let naddr = nostr.nip19.naddrEncode({ + let naddr = NostrTools.nip19.naddrEncode({ identifier: '1234', pubkey: 'c1415f950a1e3431de2bc5ee35144639e2f514cf158279abff9ed77d50118796', @@ -139,7 +132,7 @@ const market = async () => { }, async initNostr() { this.$q.loading.show() - const pool = new nostr.SimplePool() + const pool = new NostrTools.SimplePool() let relays = Array.from(this.relays) let products = new Map() let stalls = new Map() @@ -243,7 +236,7 @@ const market = async () => { let regExp = /^#([0-9a-f]{3}){1,2}$/i if (pubkey.startsWith('n')) { try { - let {type, data} = nostr.nip19.decode(pubkey) + let {type, data} = NostrTools.nip19.decode(pubkey) if (type === 'npub') pubkey = data else if (type === 'nprofile') { pubkey = data.pubkey diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html index c885e10..5f01c41 100644 --- a/templates/nostrmarket/market.html +++ b/templates/nostrmarket/market.html @@ -145,6 +145,7 @@ :products="filterProducts" :exchange-rates="exchangeRates" :product-detail="activeProduct" + :relays="relays" @change-page="navigateTo" >
-{% endblock %} {% block scripts %} - - -{% endblock %} From c041fdac5f47085bb30c7e29c02efd615bdbba7a Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Sat, 4 Mar 2023 19:49:58 +0000 Subject: [PATCH 222/891] general fixes, prop passing and shopping cart (doesn't send dm) --- .../customer-stall/customer-stall.html | 106 ++++++++++- .../customer-stall/customer-stall.js | 167 +++++++++++++++++- .../components/product-card/product-card.html | 16 ++ .../components/product-card/product-card.js | 2 +- .../product-detail/product-detail.html | 12 +- .../product-detail/product-detail.js | 15 +- .../shopping-cart/shopping-cart.html | 52 +++++- .../components/shopping-cart/shopping-cart.js | 2 +- static/js/market.js | 17 +- templates/nostrmarket/market.html | 1 + templates/nostrmarket/stall.html | 61 ------- 11 files changed, 355 insertions(+), 96 deletions(-) delete mode 100644 templates/nostrmarket/stall.html diff --git a/static/components/customer-stall/customer-stall.html b/static/components/customer-stall/customer-stall.html index 9bc302c..aedec55 100644 --- a/static/components/customer-stall/customer-stall.html +++ b/static/components/customer-stall/customer-stall.html @@ -10,13 +10,20 @@ - +
@@ -28,7 +35,102 @@ v-for="(item, idx) in products" :key="idx" > - +
+ + + + + + + + + +
+
+ Generate key pair +
+
+ Get from Extension +
+
+ + +

Select the shipping zone:

+
+ +
+
+ Total: {{ stall.currency != 'sat' ? getAmountFormated(finalCost) : + finalCost + 'sats' }} + ({{ getValueInSats(finalCost) }} sats) +
+
+ Checkout + Cancel +
+
+
+
+ diff --git a/static/components/customer-stall/customer-stall.js b/static/components/customer-stall/customer-stall.js index 1d9b395..858ed9d 100644 --- a/static/components/customer-stall/customer-stall.js +++ b/static/components/customer-stall/customer-stall.js @@ -10,23 +10,184 @@ async function customerStall(path) { 'products', 'exchange-rates', 'product-detail', - 'change-page' + 'change-page', + 'relays' ], data: function () { - return {} + return { + cart: { + total: 0, + size: 0, + products: new Map() + }, + cartMenu: [], + hasNip07: false, + checkoutDialog: { + show: false, + data: { + pubkey: null + } + }, + qrCodeDialog: { + data: { + payment_request: null + }, + show: false + } + } }, computed: { product() { if (this.productDetail) { return this.products.find(p => p.id == this.productDetail) } + }, + finalCost() { + if (!this.checkoutDialog.data.shippingzone) return this.cart.total + + let zoneCost = this.stall.shipping.find( + z => z.id == this.checkoutDialog.data.shippingzone + ) + return +this.cart.total + zoneCost.cost } }, methods: { changePageS(page, opts) { this.$emit('change-page', page, opts) + }, + getValueInSats(amount, unit = 'USD') { + if (!this.exchangeRates) return 0 + return Math.ceil( + (amount / this.exchangeRates[`BTC${unit}`][unit]) * 1e8 + ) + }, + getAmountFormated(amount, unit = 'USD') { + return LNbits.utils.formatCurrency(amount, unit) + }, + addToCart(item) { + console.log('add to cart', item) + let prod = this.cart.products + if (prod.has(item.id)) { + let qty = prod.get(item.id).quantity + prod.set(item.id, { + ...prod.get(item.id), + quantity: qty + 1 + }) + } else { + prod.set(item.id, { + name: item.name, + quantity: 1, + price: item.price, + image: item?.images[0] || null + }) + } + this.$q.notify({ + type: 'positive', + message: `${item.name} added to cart`, + icon: 'thumb_up' + }) + this.cart.products = prod + this.updateCart(+item.price) + }, + removeFromCart(item) { + this.cart.products.delete(item.id) + this.updateCart(+item.price, true) + }, + updateCart(price, del = false) { + console.log(this.cart, this.cartMenu) + if (del) { + this.cart.total -= price + this.cart.size-- + } else { + this.cart.total += price + this.cart.size++ + } + this.cartMenu = Array.from(this.cart.products, item => { + return {id: item[0], ...item[1]} + }) + console.log(this.cart, this.cartMenu) + }, + resetCart() { + this.cart = { + total: 0, + size: 0, + products: new Map() + } + }, + async getPubkey() { + try { + this.checkoutDialog.data.pubkey = await window.nostr.getPublicKey() + this.checkoutDialog.data.privkey = null + } catch (err) { + console.error( + `Failed to get a public key from a Nostr extension: ${err}` + ) + } + }, + generateKeyPair() { + let sk = NostrTools.generatePrivateKey() + let pk = NostrTools.getPublicKey(sk) + this.checkoutDialog.data.pubkey = pk + this.checkoutDialog.data.privkey = sk + }, + placeOrder() { + LNbits.utils + .confirmDialog( + `Send the order to the merchant? You should receive a message with the payment details.` + ) + .onOk(async () => { + let orderData = this.checkoutDialog.data + let content = { + name: orderData?.username, + description: null, + address: orderData.address, + message: null, + contact: { + nostr: orderData.pubkey, + phone: null, + email: orderData?.email + }, + items: Array.from(this.cart.products, p => { + return {product_id: p[0], quantity: p[1].quantity} + }) + } + let event = { + kind: 4, + created_at: Math.floor(Date.now() / 1000), + tags: [], + content: await window.nostr.nip04.encrypt( + orderData.pubkey, + content + ), + pubkey: orderData.pubkey + } + event.id = NostrTools.getEventHash(event) + if (orderData.privkey) { + event.sig = NostrTools.signEvent(event, orderData.privkey) + } else if (this.hasNip07) { + await window.nostr.signEvent(event) + } + await this.sendOrder(event) + }) + }, + async sendOrder(order) { + const pool = new NostrTools.SimplePool() + let relays = Array.from(this.relays) + let pubs = await pool.publish(relays, order) + pubs.on('ok', relay => { + console.log(`${relay.url} has accepted our event`) + }) + pubs.on('failed', reason => { + console.log(`failed to publish to ${reason}`) + }) } }, - created() {} + created() { + setTimeout(() => { + if (window.nostr) { + this.hasNip07 = true + } + }, 1000) + } }) } diff --git a/static/components/product-card/product-card.html b/static/components/product-card/product-card.html index 93d4d96..5a92ba3 100644 --- a/static/components/product-card/product-card.html +++ b/static/components/product-card/product-card.html @@ -9,6 +9,22 @@ > + Add to cart
{{ product.name }}
diff --git a/static/components/product-card/product-card.js b/static/components/product-card/product-card.js index 9e8490b..5e049df 100644 --- a/static/components/product-card/product-card.js +++ b/static/components/product-card/product-card.js @@ -4,7 +4,7 @@ async function productCard(path) { name: 'product-card', template, - props: ['product', 'change-page'], + props: ['product', 'change-page', 'add-to-cart', 'is-stall'], data: function () { return {} }, diff --git a/static/components/product-detail/product-detail.html b/static/components/product-detail/product-detail.html index 92fa1c0..be31eda 100644 --- a/static/components/product-detail/product-detail.html +++ b/static/components/product-detail/product-detail.html @@ -17,6 +17,11 @@ style="/*background-size: contain; background-repeat: no-repeat*/" > +
@@ -47,7 +52,7 @@ {{ product.amount > 0 ? 'In stock.' : 'Out of stock.' }}{{ product.quantity > 0 ? 'In stock.' : 'Out of stock.' }}
@@ -56,12 +61,13 @@ color="primary" icon="shopping_cart" label="Add to cart" + @click="$emit('add-to-cart', product)" />
diff --git a/static/components/product-detail/product-detail.js b/static/components/product-detail/product-detail.js index 7b60f6b..d55b653 100644 --- a/static/components/product-detail/product-detail.js +++ b/static/components/product-detail/product-detail.js @@ -4,23 +4,14 @@ async function productDetail(path) { name: 'product-detail', template, - props: ['product'], + props: ['product', 'add-to-cart'], data: function () { return { slide: 1 } }, - computed: { - win_width() { - return this.$q.screen.width - 59 - }, - win_height() { - return this.$q.screen.height - 0 - } - }, + computed: {}, methods: {}, - created() { - console.log('ping') - } + created() {} }) } diff --git a/static/components/shopping-cart/shopping-cart.html b/static/components/shopping-cart/shopping-cart.html index 8650cc6..2864cf3 100644 --- a/static/components/shopping-cart/shopping-cart.html +++ b/static/components/shopping-cart/shopping-cart.html @@ -1 +1,51 @@ - + + + {{ cart.size }} + + + + + + {{p.quantity}} x + + + + + + + + + {{ p.name }} + + + + + {{p.currency != 'sat' ? p.formatedPrice : p.price + 'sats'}} + + + + + + +
+ +
diff --git a/static/components/shopping-cart/shopping-cart.js b/static/components/shopping-cart/shopping-cart.js index e0ad053..8f6902d 100644 --- a/static/components/shopping-cart/shopping-cart.js +++ b/static/components/shopping-cart/shopping-cart.js @@ -5,7 +5,7 @@ async function shoppingCart(path) { name: 'shopping-cart', template, - props: [], + props: ['cart', 'cart-menu', 'remove-from-cart', 'reset-cart'], data: function () { return {} }, diff --git a/static/js/market.js b/static/js/market.js index 6a0d723..95bf5a6 100644 --- a/static/js/market.js +++ b/static/js/market.js @@ -1,20 +1,13 @@ const market = async () => { Vue.component(VueQrcode.name, VueQrcode) - const nostr = window.NostrTools + const NostrTools = window.NostrTools const defaultRelays = [ 'wss://relay.damus.io', 'wss://relay.snort.social', - 'wss://nos.lol', 'wss://nostr.wine', - 'wss://relay.nostr.bg', 'wss://nostr-pub.wellorder.net', - 'wss://nostr-pub.semisol.dev', - 'wss://eden.nostr.land', - 'wss://nostr.mom', - 'wss://nostr.fmt.wiz.biz', - 'wss://nostr.zebedee.cloud', - 'wss://nostr.rocks' + 'wss://nostr.zebedee.cloud' ] const eventToObj = event => { event.content = JSON.parse(event.content) @@ -128,7 +121,7 @@ const market = async () => { }, methods: { naddr() { - let naddr = nostr.nip19.naddrEncode({ + let naddr = NostrTools.nip19.naddrEncode({ identifier: '1234', pubkey: 'c1415f950a1e3431de2bc5ee35144639e2f514cf158279abff9ed77d50118796', @@ -139,7 +132,7 @@ const market = async () => { }, async initNostr() { this.$q.loading.show() - const pool = new nostr.SimplePool() + const pool = new NostrTools.SimplePool() let relays = Array.from(this.relays) let products = new Map() let stalls = new Map() @@ -243,7 +236,7 @@ const market = async () => { let regExp = /^#([0-9a-f]{3}){1,2}$/i if (pubkey.startsWith('n')) { try { - let {type, data} = nostr.nip19.decode(pubkey) + let {type, data} = NostrTools.nip19.decode(pubkey) if (type === 'npub') pubkey = data else if (type === 'nprofile') { pubkey = data.pubkey diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html index c885e10..5f01c41 100644 --- a/templates/nostrmarket/market.html +++ b/templates/nostrmarket/market.html @@ -145,6 +145,7 @@ :products="filterProducts" :exchange-rates="exchangeRates" :product-detail="activeProduct" + :relays="relays" @change-page="navigateTo" >
-{% endblock %} {% block scripts %} - - -{% endblock %} From da404a6816577569b3911c16b104e84fd2a1b35c Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Sun, 5 Mar 2023 20:32:35 +0000 Subject: [PATCH 223/891] ordering working --- .../customer-stall/customer-stall.js | 138 +++++++++++------- static/js/market.js | 2 +- 2 files changed, 89 insertions(+), 51 deletions(-) diff --git a/static/components/customer-stall/customer-stall.js b/static/components/customer-stall/customer-stall.js index 858ed9d..ce2be6e 100644 --- a/static/components/customer-stall/customer-stall.js +++ b/static/components/customer-stall/customer-stall.js @@ -24,9 +24,7 @@ async function customerStall(path) { hasNip07: false, checkoutDialog: { show: false, - data: { - pubkey: null - } + data: {} }, qrCodeDialog: { data: { @@ -130,56 +128,96 @@ async function customerStall(path) { this.checkoutDialog.data.pubkey = pk this.checkoutDialog.data.privkey = sk }, - placeOrder() { - LNbits.utils - .confirmDialog( - `Send the order to the merchant? You should receive a message with the payment details.` - ) - .onOk(async () => { - let orderData = this.checkoutDialog.data - let content = { - name: orderData?.username, - description: null, - address: orderData.address, - message: null, - contact: { - nostr: orderData.pubkey, - phone: null, - email: orderData?.email - }, - items: Array.from(this.cart.products, p => { - return {product_id: p[0], quantity: p[1].quantity} - }) - } - let event = { - kind: 4, - created_at: Math.floor(Date.now() / 1000), - tags: [], - content: await window.nostr.nip04.encrypt( - orderData.pubkey, - content - ), - pubkey: orderData.pubkey - } - event.id = NostrTools.getEventHash(event) - if (orderData.privkey) { - event.sig = NostrTools.signEvent(event, orderData.privkey) - } else if (this.hasNip07) { - await window.nostr.signEvent(event) - } - await this.sendOrder(event) + async placeOrder() { + // LNbits.utils + // .confirmDialog( + // `Send the order to the merchant? You should receive a message with the payment details.` + // ) + // .onOk(async () => { + let orderData = this.checkoutDialog.data + let orderObj = { + name: orderData?.username, + description: null, + address: orderData.address, + message: null, + contact: { + nostr: orderData.pubkey, + phone: null, + email: orderData?.email + }, + items: Array.from(this.cart.products, p => { + return {product_id: p[0], quantity: p[1].quantity} }) + } + let event = { + ...(await NostrTools.getBlankEvent()), + kind: 4, + created_at: Math.floor(Date.now() / 1000), + tags: [['p', this.stall.pubkey]], + pubkey: orderData.pubkey + } + if (orderData.privkey) { + event.content = await NostrTools.nip04.encrypt( + orderData.privkey, + this.stall.pubkey, + JSON.stringify(orderObj) + ) + } else { + console.log('use extension') + event.content = await window.nostr.nip04.encrypt( + orderData.pubkey, + JSON.stringify(orderObj) + ) + let userRelays = Object.keys( + (await window.nostr?.getRelays?.()) || [] + ) + if (userRelays.length != 0) { + userRelays.map(r => this.relays.add(r)) + } + } + event.id = NostrTools.getEventHash(event) + if (orderData.privkey) { + event.sig = await NostrTools.signEvent(event, orderData.privkey) + } else if (this.hasNip07) { + event = await window.nostr.signEvent(event) + } + console.log(event, orderData) + await this.sendOrder(event) + // }) }, async sendOrder(order) { - const pool = new NostrTools.SimplePool() - let relays = Array.from(this.relays) - let pubs = await pool.publish(relays, order) - pubs.on('ok', relay => { - console.log(`${relay.url} has accepted our event`) - }) - pubs.on('failed', reason => { - console.log(`failed to publish to ${reason}`) - }) + for (const url of Array.from(this.relays)) { + let relay = NostrTools.relayInit(url) + relay.on('connect', () => { + console.log(`connected to ${relay.url}`) + }) + relay.on('error', () => { + console.log(`failed to connect to ${relay.url}`) + }) + + await relay.connect() + let pub = relay.publish(order) + pub.on('ok', () => { + console.log(`${relay.url} has accepted our event`) + }) + pub.on('failed', reason => { + console.log(`failed to publish to ${relay.url}: ${reason}`) + }) + } + this.checkoutDialog = {show: false, data: {}} + // const pool = new NostrTools.SimplePool() + // let relays = Array.from(this.relays) + // try { + // let pubs = await pool.publish(relays, order) + // pubs.on('ok', relay => { + // console.log(`${relay.url} has accepted our event`) + // }) + // pubs.on('failed', (reason, err) => { + // console.log(`failed to publish to ${reason}: ${err}`) + // }) + // } catch (err) { + // console.error(err) + // } } }, created() { diff --git a/static/js/market.js b/static/js/market.js index 95bf5a6..a7c724e 100644 --- a/static/js/market.js +++ b/static/js/market.js @@ -5,7 +5,6 @@ const market = async () => { const defaultRelays = [ 'wss://relay.damus.io', 'wss://relay.snort.social', - 'wss://nostr.wine', 'wss://nostr-pub.wellorder.net', 'wss://nostr.zebedee.cloud' ] @@ -175,6 +174,7 @@ const market = async () => { return obj }) pool.close(relays) + return }, async getRates() { let noFiat = this.stalls.map(s => s.currency).every(c => c == 'sat') From 7a62631e5751fcd88e1f6eec6d4f7b0864680662 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Sun, 5 Mar 2023 20:32:35 +0000 Subject: [PATCH 224/891] ordering working --- .../customer-stall/customer-stall.js | 138 +++++++++++------- static/js/market.js | 2 +- 2 files changed, 89 insertions(+), 51 deletions(-) diff --git a/static/components/customer-stall/customer-stall.js b/static/components/customer-stall/customer-stall.js index 858ed9d..ce2be6e 100644 --- a/static/components/customer-stall/customer-stall.js +++ b/static/components/customer-stall/customer-stall.js @@ -24,9 +24,7 @@ async function customerStall(path) { hasNip07: false, checkoutDialog: { show: false, - data: { - pubkey: null - } + data: {} }, qrCodeDialog: { data: { @@ -130,56 +128,96 @@ async function customerStall(path) { this.checkoutDialog.data.pubkey = pk this.checkoutDialog.data.privkey = sk }, - placeOrder() { - LNbits.utils - .confirmDialog( - `Send the order to the merchant? You should receive a message with the payment details.` - ) - .onOk(async () => { - let orderData = this.checkoutDialog.data - let content = { - name: orderData?.username, - description: null, - address: orderData.address, - message: null, - contact: { - nostr: orderData.pubkey, - phone: null, - email: orderData?.email - }, - items: Array.from(this.cart.products, p => { - return {product_id: p[0], quantity: p[1].quantity} - }) - } - let event = { - kind: 4, - created_at: Math.floor(Date.now() / 1000), - tags: [], - content: await window.nostr.nip04.encrypt( - orderData.pubkey, - content - ), - pubkey: orderData.pubkey - } - event.id = NostrTools.getEventHash(event) - if (orderData.privkey) { - event.sig = NostrTools.signEvent(event, orderData.privkey) - } else if (this.hasNip07) { - await window.nostr.signEvent(event) - } - await this.sendOrder(event) + async placeOrder() { + // LNbits.utils + // .confirmDialog( + // `Send the order to the merchant? You should receive a message with the payment details.` + // ) + // .onOk(async () => { + let orderData = this.checkoutDialog.data + let orderObj = { + name: orderData?.username, + description: null, + address: orderData.address, + message: null, + contact: { + nostr: orderData.pubkey, + phone: null, + email: orderData?.email + }, + items: Array.from(this.cart.products, p => { + return {product_id: p[0], quantity: p[1].quantity} }) + } + let event = { + ...(await NostrTools.getBlankEvent()), + kind: 4, + created_at: Math.floor(Date.now() / 1000), + tags: [['p', this.stall.pubkey]], + pubkey: orderData.pubkey + } + if (orderData.privkey) { + event.content = await NostrTools.nip04.encrypt( + orderData.privkey, + this.stall.pubkey, + JSON.stringify(orderObj) + ) + } else { + console.log('use extension') + event.content = await window.nostr.nip04.encrypt( + orderData.pubkey, + JSON.stringify(orderObj) + ) + let userRelays = Object.keys( + (await window.nostr?.getRelays?.()) || [] + ) + if (userRelays.length != 0) { + userRelays.map(r => this.relays.add(r)) + } + } + event.id = NostrTools.getEventHash(event) + if (orderData.privkey) { + event.sig = await NostrTools.signEvent(event, orderData.privkey) + } else if (this.hasNip07) { + event = await window.nostr.signEvent(event) + } + console.log(event, orderData) + await this.sendOrder(event) + // }) }, async sendOrder(order) { - const pool = new NostrTools.SimplePool() - let relays = Array.from(this.relays) - let pubs = await pool.publish(relays, order) - pubs.on('ok', relay => { - console.log(`${relay.url} has accepted our event`) - }) - pubs.on('failed', reason => { - console.log(`failed to publish to ${reason}`) - }) + for (const url of Array.from(this.relays)) { + let relay = NostrTools.relayInit(url) + relay.on('connect', () => { + console.log(`connected to ${relay.url}`) + }) + relay.on('error', () => { + console.log(`failed to connect to ${relay.url}`) + }) + + await relay.connect() + let pub = relay.publish(order) + pub.on('ok', () => { + console.log(`${relay.url} has accepted our event`) + }) + pub.on('failed', reason => { + console.log(`failed to publish to ${relay.url}: ${reason}`) + }) + } + this.checkoutDialog = {show: false, data: {}} + // const pool = new NostrTools.SimplePool() + // let relays = Array.from(this.relays) + // try { + // let pubs = await pool.publish(relays, order) + // pubs.on('ok', relay => { + // console.log(`${relay.url} has accepted our event`) + // }) + // pubs.on('failed', (reason, err) => { + // console.log(`failed to publish to ${reason}: ${err}`) + // }) + // } catch (err) { + // console.error(err) + // } } }, created() { diff --git a/static/js/market.js b/static/js/market.js index 95bf5a6..a7c724e 100644 --- a/static/js/market.js +++ b/static/js/market.js @@ -5,7 +5,6 @@ const market = async () => { const defaultRelays = [ 'wss://relay.damus.io', 'wss://relay.snort.social', - 'wss://nostr.wine', 'wss://nostr-pub.wellorder.net', 'wss://nostr.zebedee.cloud' ] @@ -175,6 +174,7 @@ const market = async () => { return obj }) pool.close(relays) + return }, async getRates() { let noFiat = this.stalls.map(s => s.currency).every(c => c == 'sat') From 9e7f7e584d8cd3cb2c390e3943398f092910df9c Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Mon, 6 Mar 2023 09:59:17 +0000 Subject: [PATCH 225/891] hash string --- static/js/utils.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/static/js/utils.js b/static/js/utils.js index 83e886b..3e3a14e 100644 --- a/static/js/utils.js +++ b/static/js/utils.js @@ -25,3 +25,13 @@ function imgSizeFit(img, maxWidth = 1024, maxHeight = 768) { ) return {width: img.naturalWidth * ratio, height: img.naturalHeight * ratio} } + +async function hash(string) { + const utf8 = new TextEncoder().encode(string) + const hashBuffer = await crypto.subtle.digest('SHA-256', utf8) + const hashArray = Array.from(new Uint8Array(hashBuffer)) + const hashHex = hashArray + .map(bytes => bytes.toString(16).padStart(2, '0')) + .join('') + return hashHex +} From 7cdbc5b752a605e7734acbd732b456fe8205aa00 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Mon, 6 Mar 2023 09:59:17 +0000 Subject: [PATCH 226/891] hash string --- static/js/utils.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/static/js/utils.js b/static/js/utils.js index 83e886b..3e3a14e 100644 --- a/static/js/utils.js +++ b/static/js/utils.js @@ -25,3 +25,13 @@ function imgSizeFit(img, maxWidth = 1024, maxHeight = 768) { ) return {width: img.naturalWidth * ratio, height: img.naturalHeight * ratio} } + +async function hash(string) { + const utf8 = new TextEncoder().encode(string) + const hashBuffer = await crypto.subtle.digest('SHA-256', utf8) + const hashArray = Array.from(new Uint8Array(hashBuffer)) + const hashHex = hashArray + .map(bytes => bytes.toString(16).padStart(2, '0')) + .join('') + return hashHex +} From e0345be18ec725de90f134fa245ce7bf6b10ddc3 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Mon, 6 Mar 2023 12:03:32 +0000 Subject: [PATCH 227/891] send/receive messages --- .../customer-stall/customer-stall.html | 7 + .../customer-stall/customer-stall.js | 239 +++++++++++------- 2 files changed, 159 insertions(+), 87 deletions(-) diff --git a/static/components/customer-stall/customer-stall.html b/static/components/customer-stall/customer-stall.html index aedec55..c6ac229 100644 --- a/static/components/customer-stall/customer-stall.html +++ b/static/components/customer-stall/customer-stall.html @@ -95,6 +95,13 @@ label="Email *optional" hint="Merchant may not use email" > +

Select the shipping zone:

{ - let orderData = this.checkoutDialog.data - let orderObj = { - name: orderData?.username, - description: null, - address: orderData.address, - message: null, - contact: { - nostr: orderData.pubkey, - phone: null, - email: orderData?.email - }, - items: Array.from(this.cart.products, p => { - return {product_id: p[0], quantity: p[1].quantity} + LNbits.utils + .confirmDialog( + `Send the order to the merchant? You should receive a message with the payment details.` + ) + .onOk(async () => { + let orderData = this.checkoutDialog.data + let orderObj = { + name: orderData?.username, + address: orderData.address, + message: orderData?.message, + contact: { + nostr: this.customerPubkey, + phone: null, + email: orderData?.email + }, + items: Array.from(this.cart.products, p => { + return {product_id: p[0], quantity: p[1].quantity} + }) + } + let created_at = Math.floor(Date.now() / 1000) + orderObj.id = await hash( + [this.customerPubkey, created_at, JSON.stringify(orderObj)].join( + ':' + ) + ) + let event = { + ...(await NostrTools.getBlankEvent()), + kind: 4, + created_at, + tags: [['p', this.stall.pubkey]], + pubkey: this.customerPubkey + } + if (this.customerPrivKey) { + event.content = await NostrTools.nip04.encrypt( + this.customerPrivKey, + this.stall.pubkey, + JSON.stringify(orderObj) + ) + } else { + event.content = await window.nostr.nip04.encrypt( + this.stall.pubkey, + JSON.stringify(orderObj) + ) + let userRelays = Object.keys( + (await window.nostr?.getRelays?.()) || [] + ) + if (userRelays.length != 0) { + userRelays.map(r => this.relays.add(r)) + } + } + event.id = NostrTools.getEventHash(event) + if (this.customerPrivKey) { + event.sig = await NostrTools.signEvent( + event, + this.customerPrivKey + ) + } else if (this.hasNip07) { + event = await window.nostr.signEvent(event) + } + console.log(event, orderObj) + await this.sendOrder(event) }) - } - let event = { - ...(await NostrTools.getBlankEvent()), - kind: 4, - created_at: Math.floor(Date.now() / 1000), - tags: [['p', this.stall.pubkey]], - pubkey: orderData.pubkey - } - if (orderData.privkey) { - event.content = await NostrTools.nip04.encrypt( - orderData.privkey, - this.stall.pubkey, - JSON.stringify(orderObj) - ) - } else { - console.log('use extension') - event.content = await window.nostr.nip04.encrypt( - orderData.pubkey, - JSON.stringify(orderObj) - ) - let userRelays = Object.keys( - (await window.nostr?.getRelays?.()) || [] - ) - if (userRelays.length != 0) { - userRelays.map(r => this.relays.add(r)) - } - } - event.id = NostrTools.getEventHash(event) - if (orderData.privkey) { - event.sig = await NostrTools.signEvent(event, orderData.privkey) - } else if (this.hasNip07) { - event = await window.nostr.signEvent(event) - } - console.log(event, orderData) - await this.sendOrder(event) - // }) }, async sendOrder(order) { for (const url of Array.from(this.relays)) { - let relay = NostrTools.relayInit(url) - relay.on('connect', () => { - console.log(`connected to ${relay.url}`) - }) - relay.on('error', () => { - console.log(`failed to connect to ${relay.url}`) - }) + try { + let relay = NostrTools.relayInit(url) + relay.on('connect', () => { + console.log(`connected to ${relay.url}`) + }) + relay.on('error', () => { + console.log(`failed to connect to ${relay.url}`) + }) - await relay.connect() - let pub = relay.publish(order) - pub.on('ok', () => { - console.log(`${relay.url} has accepted our event`) - }) - pub.on('failed', reason => { - console.log(`failed to publish to ${relay.url}: ${reason}`) - }) + await relay.connect() + let pub = relay.publish(order) + pub.on('ok', () => { + console.log(`${relay.url} has accepted our event`) + }) + pub.on('failed', reason => { + console.log(`failed to publish to ${relay.url}: ${reason}`) + }) + } catch (err) { + console.error(`Error: ${err}`) + } + } + this.resetCheckout() + this.listenMessages() + }, + async listenMessages() { + try { + const pool = new NostrTools.SimplePool() + const filters = [ + { + kinds: [4], + authors: [this.customerPubkey] + }, + { + kinds: [4], + '#p': [this.customerPubkey] + } + ] + let relays = Array.from(this.relays) + let subs = pool.sub(relays, filters) + subs.on('event', async event => { + let mine = event.pubkey == this.customerPubkey + let sender = mine + ? event.tags.find(([k, v]) => k === 'p' && v && v !== '')[1] + : event.pubkey + if ( + (mine && sender != this.stall.pubkey) || + (!mine && sender != this.customerPubkey) + ) { + console.log(`Not relevant message!`) + return + } + try { + let plaintext = this.customerPrivKey + ? await NostrTools.nip04.decrypt( + this.customerPrivKey, + sender, + event.content + ) + : await window.nostr.nip04.decrypt(sender, event.content) + // console.log(`${mine ? 'Me' : 'Customer'}: ${plaintext}`) + this.nostrMessages.set(event.id, { + msg: plaintext, + timestamp: event.created_at, + sender: `${mine ? 'Me' : 'Merchant'}` + }) + } catch { + console.error('Unable to decrypt message!') + return + } + }) + } catch (err) { + console.error(`Error: ${err}`) } - this.checkoutDialog = {show: false, data: {}} - // const pool = new NostrTools.SimplePool() - // let relays = Array.from(this.relays) - // try { - // let pubs = await pool.publish(relays, order) - // pubs.on('ok', relay => { - // console.log(`${relay.url} has accepted our event`) - // }) - // pubs.on('failed', (reason, err) => { - // console.log(`failed to publish to ${reason}: ${err}`) - // }) - // } catch (err) { - // console.error(err) - // } } }, created() { From 040158866897903760aed6d0a7f3ca635f637c74 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Mon, 6 Mar 2023 12:03:32 +0000 Subject: [PATCH 228/891] send/receive messages --- .../customer-stall/customer-stall.html | 7 + .../customer-stall/customer-stall.js | 239 +++++++++++------- 2 files changed, 159 insertions(+), 87 deletions(-) diff --git a/static/components/customer-stall/customer-stall.html b/static/components/customer-stall/customer-stall.html index aedec55..c6ac229 100644 --- a/static/components/customer-stall/customer-stall.html +++ b/static/components/customer-stall/customer-stall.html @@ -95,6 +95,13 @@ label="Email *optional" hint="Merchant may not use email" > +

Select the shipping zone:

{ - let orderData = this.checkoutDialog.data - let orderObj = { - name: orderData?.username, - description: null, - address: orderData.address, - message: null, - contact: { - nostr: orderData.pubkey, - phone: null, - email: orderData?.email - }, - items: Array.from(this.cart.products, p => { - return {product_id: p[0], quantity: p[1].quantity} + LNbits.utils + .confirmDialog( + `Send the order to the merchant? You should receive a message with the payment details.` + ) + .onOk(async () => { + let orderData = this.checkoutDialog.data + let orderObj = { + name: orderData?.username, + address: orderData.address, + message: orderData?.message, + contact: { + nostr: this.customerPubkey, + phone: null, + email: orderData?.email + }, + items: Array.from(this.cart.products, p => { + return {product_id: p[0], quantity: p[1].quantity} + }) + } + let created_at = Math.floor(Date.now() / 1000) + orderObj.id = await hash( + [this.customerPubkey, created_at, JSON.stringify(orderObj)].join( + ':' + ) + ) + let event = { + ...(await NostrTools.getBlankEvent()), + kind: 4, + created_at, + tags: [['p', this.stall.pubkey]], + pubkey: this.customerPubkey + } + if (this.customerPrivKey) { + event.content = await NostrTools.nip04.encrypt( + this.customerPrivKey, + this.stall.pubkey, + JSON.stringify(orderObj) + ) + } else { + event.content = await window.nostr.nip04.encrypt( + this.stall.pubkey, + JSON.stringify(orderObj) + ) + let userRelays = Object.keys( + (await window.nostr?.getRelays?.()) || [] + ) + if (userRelays.length != 0) { + userRelays.map(r => this.relays.add(r)) + } + } + event.id = NostrTools.getEventHash(event) + if (this.customerPrivKey) { + event.sig = await NostrTools.signEvent( + event, + this.customerPrivKey + ) + } else if (this.hasNip07) { + event = await window.nostr.signEvent(event) + } + console.log(event, orderObj) + await this.sendOrder(event) }) - } - let event = { - ...(await NostrTools.getBlankEvent()), - kind: 4, - created_at: Math.floor(Date.now() / 1000), - tags: [['p', this.stall.pubkey]], - pubkey: orderData.pubkey - } - if (orderData.privkey) { - event.content = await NostrTools.nip04.encrypt( - orderData.privkey, - this.stall.pubkey, - JSON.stringify(orderObj) - ) - } else { - console.log('use extension') - event.content = await window.nostr.nip04.encrypt( - orderData.pubkey, - JSON.stringify(orderObj) - ) - let userRelays = Object.keys( - (await window.nostr?.getRelays?.()) || [] - ) - if (userRelays.length != 0) { - userRelays.map(r => this.relays.add(r)) - } - } - event.id = NostrTools.getEventHash(event) - if (orderData.privkey) { - event.sig = await NostrTools.signEvent(event, orderData.privkey) - } else if (this.hasNip07) { - event = await window.nostr.signEvent(event) - } - console.log(event, orderData) - await this.sendOrder(event) - // }) }, async sendOrder(order) { for (const url of Array.from(this.relays)) { - let relay = NostrTools.relayInit(url) - relay.on('connect', () => { - console.log(`connected to ${relay.url}`) - }) - relay.on('error', () => { - console.log(`failed to connect to ${relay.url}`) - }) + try { + let relay = NostrTools.relayInit(url) + relay.on('connect', () => { + console.log(`connected to ${relay.url}`) + }) + relay.on('error', () => { + console.log(`failed to connect to ${relay.url}`) + }) - await relay.connect() - let pub = relay.publish(order) - pub.on('ok', () => { - console.log(`${relay.url} has accepted our event`) - }) - pub.on('failed', reason => { - console.log(`failed to publish to ${relay.url}: ${reason}`) - }) + await relay.connect() + let pub = relay.publish(order) + pub.on('ok', () => { + console.log(`${relay.url} has accepted our event`) + }) + pub.on('failed', reason => { + console.log(`failed to publish to ${relay.url}: ${reason}`) + }) + } catch (err) { + console.error(`Error: ${err}`) + } + } + this.resetCheckout() + this.listenMessages() + }, + async listenMessages() { + try { + const pool = new NostrTools.SimplePool() + const filters = [ + { + kinds: [4], + authors: [this.customerPubkey] + }, + { + kinds: [4], + '#p': [this.customerPubkey] + } + ] + let relays = Array.from(this.relays) + let subs = pool.sub(relays, filters) + subs.on('event', async event => { + let mine = event.pubkey == this.customerPubkey + let sender = mine + ? event.tags.find(([k, v]) => k === 'p' && v && v !== '')[1] + : event.pubkey + if ( + (mine && sender != this.stall.pubkey) || + (!mine && sender != this.customerPubkey) + ) { + console.log(`Not relevant message!`) + return + } + try { + let plaintext = this.customerPrivKey + ? await NostrTools.nip04.decrypt( + this.customerPrivKey, + sender, + event.content + ) + : await window.nostr.nip04.decrypt(sender, event.content) + // console.log(`${mine ? 'Me' : 'Customer'}: ${plaintext}`) + this.nostrMessages.set(event.id, { + msg: plaintext, + timestamp: event.created_at, + sender: `${mine ? 'Me' : 'Merchant'}` + }) + } catch { + console.error('Unable to decrypt message!') + return + } + }) + } catch (err) { + console.error(`Error: ${err}`) } - this.checkoutDialog = {show: false, data: {}} - // const pool = new NostrTools.SimplePool() - // let relays = Array.from(this.relays) - // try { - // let pubs = await pool.publish(relays, order) - // pubs.on('ok', relay => { - // console.log(`${relay.url} has accepted our event`) - // }) - // pubs.on('failed', (reason, err) => { - // console.log(`failed to publish to ${reason}: ${err}`) - // }) - // } catch (err) { - // console.error(err) - // } } }, created() { From cc6c59253f532d41ed116587834caaee72f12805 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Tue, 7 Mar 2023 13:25:16 +0000 Subject: [PATCH 229/891] login/account functionality --- static/js/market.js | 70 ++++++++++++++++- templates/nostrmarket/market.html | 120 ++++++++++++++++++++++++++++++ 2 files changed, 187 insertions(+), 3 deletions(-) diff --git a/static/js/market.js b/static/js/market.js index a7c724e..88042ce 100644 --- a/static/js/market.js +++ b/static/js/market.js @@ -2,6 +2,7 @@ const market = async () => { Vue.component(VueQrcode.name, VueQrcode) const NostrTools = window.NostrTools + const defaultRelays = [ 'wss://relay.damus.io', 'wss://relay.snort.social', @@ -23,7 +24,8 @@ const market = async () => { customerMarket('static/components/customer-market/customer-market.html'), customerStall('static/components/customer-stall/customer-stall.html'), productDetail('static/components/product-detail/product-detail.html'), - shoppingCart('static/components/shopping-cart/shopping-cart.html') + shoppingCart('static/components/shopping-cart/shopping-cart.html'), + chatDialog('static/components/chat-dialog/chat-dialog.html') ]) new Vue({ @@ -31,7 +33,15 @@ const market = async () => { mixins: [windowMixin], data: function () { return { - drawer: false, + account: null, + accountDialog: { + show: false, + data: { + watchOnly: false, + key: null + } + }, + drawer: true, pubkeys: new Set(), relays: new Set(), events: [], @@ -72,9 +82,20 @@ const market = async () => { }, isLoading() { return this.$q.loading.isActive + }, + hasExtension() { + return window.nostr + }, + isValidKey() { + return this.accountDialog.data.key + ?.toLowerCase() + ?.match(/^[0-9a-f]{64}$/) } }, async created() { + // Check for user stored + this.account = this.$q.localStorage.getItem('diagonAlley.account') || null + // Check for stored merchants and relays on localStorage try { let merchants = this.$q.localStorage.getItem(`diagonAlley.merchants`) @@ -115,7 +136,10 @@ const market = async () => { } // Get notes from Nostr - await this.initNostr() + //await this.initNostr() + + // Get fiat rates (i think there's an LNbits endpoint for this) + //await this.getRates() this.$q.loading.hide() }, methods: { @@ -129,6 +153,46 @@ const market = async () => { }) console.log(naddr) }, + async deleteAccount() { + await LNbits.utils + .confirmDialog( + `This will delete all stored data. If you didn't backup the Key Pair (Private and Public Keys), you will lose it. Continue?` + ) + .onOk(() => { + window.localStorage.removeItem('diagonAlley.account') + this.account = null + }) + }, + async createAccount(useExtension = false) { + let nip07 + if (useExtension) { + await this.getFromExtension() + nip07 = true + } + if (this.isValidKey) { + let {key, watchOnly} = this.accountDialog.data + this.$q.localStorage.set('diagonAlley.account', { + privkey: watchOnly ? null : key, + pubkey: watchOnly ? key : NostrTools.getPublicKey(key), + useExtension: nip07 ?? false + }) + this.accountDialog.data = { + watchOnly: false, + key: null + } + this.accountDialog.show = false + this.account = this.$q.localStorage.getItem('diagonAlley.account') + } + }, + generateKeyPair() { + this.accountDialog.data.key = NostrTools.generatePrivateKey() + this.accountDialog.data.watchOnly = false + }, + async getFromExtension() { + this.accountDialog.data.key = await window.nostr.getPublicKey() + this.accountDialog.data.watchOnly = true + return + }, async initNostr() { this.$q.loading.show() const pool = new NostrTools.SimplePool() diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html index 5f01c41..77c0cac 100644 --- a/templates/nostrmarket/market.html +++ b/templates/nostrmarket/market.html @@ -5,6 +5,32 @@ Settings +
+
+ + Delete account data +
+
+ Login or Create account +
+ +
+ + + + +
Account Setup
+ + +
+ +

Type your Nostr private key or generate a new one.

+ You can also use a Nostr-capable extension. +
+ + + + + + + + + + Is this a Public Key? + + If not using an Nostr capable extension, you'll have to sign + events manually! Better to use a Private Key that you can delete + later, or just generate an ephemeral key pair to use in the + Marketplace! + + + + + + + + + + +
+
{% endblock %} {% block scripts %} @@ -165,6 +247,44 @@ + + {% endblock %} From 60f3edc25af20bc08a280007da6c0ea548c3e3b4 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Tue, 7 Mar 2023 13:25:16 +0000 Subject: [PATCH 230/891] login/account functionality --- static/js/market.js | 70 ++++++++++++++++- templates/nostrmarket/market.html | 120 ++++++++++++++++++++++++++++++ 2 files changed, 187 insertions(+), 3 deletions(-) diff --git a/static/js/market.js b/static/js/market.js index a7c724e..88042ce 100644 --- a/static/js/market.js +++ b/static/js/market.js @@ -2,6 +2,7 @@ const market = async () => { Vue.component(VueQrcode.name, VueQrcode) const NostrTools = window.NostrTools + const defaultRelays = [ 'wss://relay.damus.io', 'wss://relay.snort.social', @@ -23,7 +24,8 @@ const market = async () => { customerMarket('static/components/customer-market/customer-market.html'), customerStall('static/components/customer-stall/customer-stall.html'), productDetail('static/components/product-detail/product-detail.html'), - shoppingCart('static/components/shopping-cart/shopping-cart.html') + shoppingCart('static/components/shopping-cart/shopping-cart.html'), + chatDialog('static/components/chat-dialog/chat-dialog.html') ]) new Vue({ @@ -31,7 +33,15 @@ const market = async () => { mixins: [windowMixin], data: function () { return { - drawer: false, + account: null, + accountDialog: { + show: false, + data: { + watchOnly: false, + key: null + } + }, + drawer: true, pubkeys: new Set(), relays: new Set(), events: [], @@ -72,9 +82,20 @@ const market = async () => { }, isLoading() { return this.$q.loading.isActive + }, + hasExtension() { + return window.nostr + }, + isValidKey() { + return this.accountDialog.data.key + ?.toLowerCase() + ?.match(/^[0-9a-f]{64}$/) } }, async created() { + // Check for user stored + this.account = this.$q.localStorage.getItem('diagonAlley.account') || null + // Check for stored merchants and relays on localStorage try { let merchants = this.$q.localStorage.getItem(`diagonAlley.merchants`) @@ -115,7 +136,10 @@ const market = async () => { } // Get notes from Nostr - await this.initNostr() + //await this.initNostr() + + // Get fiat rates (i think there's an LNbits endpoint for this) + //await this.getRates() this.$q.loading.hide() }, methods: { @@ -129,6 +153,46 @@ const market = async () => { }) console.log(naddr) }, + async deleteAccount() { + await LNbits.utils + .confirmDialog( + `This will delete all stored data. If you didn't backup the Key Pair (Private and Public Keys), you will lose it. Continue?` + ) + .onOk(() => { + window.localStorage.removeItem('diagonAlley.account') + this.account = null + }) + }, + async createAccount(useExtension = false) { + let nip07 + if (useExtension) { + await this.getFromExtension() + nip07 = true + } + if (this.isValidKey) { + let {key, watchOnly} = this.accountDialog.data + this.$q.localStorage.set('diagonAlley.account', { + privkey: watchOnly ? null : key, + pubkey: watchOnly ? key : NostrTools.getPublicKey(key), + useExtension: nip07 ?? false + }) + this.accountDialog.data = { + watchOnly: false, + key: null + } + this.accountDialog.show = false + this.account = this.$q.localStorage.getItem('diagonAlley.account') + } + }, + generateKeyPair() { + this.accountDialog.data.key = NostrTools.generatePrivateKey() + this.accountDialog.data.watchOnly = false + }, + async getFromExtension() { + this.accountDialog.data.key = await window.nostr.getPublicKey() + this.accountDialog.data.watchOnly = true + return + }, async initNostr() { this.$q.loading.show() const pool = new NostrTools.SimplePool() diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html index 5f01c41..77c0cac 100644 --- a/templates/nostrmarket/market.html +++ b/templates/nostrmarket/market.html @@ -5,6 +5,32 @@ Settings +
+
+ + Delete account data +
+
+ Login or Create account +
+ +
+ + + + +
Account Setup
+ + +
+ +

Type your Nostr private key or generate a new one.

+ You can also use a Nostr-capable extension. +
+ + + + + + + + + + Is this a Public Key? + + If not using an Nostr capable extension, you'll have to sign + events manually! Better to use a Private Key that you can delete + later, or just generate an ephemeral key pair to use in the + Marketplace! + + + + + + + + + + +
+
{% endblock %} {% block scripts %} @@ -165,6 +247,44 @@ + + {% endblock %} From fbb45ed9f7351d306c132da2c365911d8fb484bf Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Wed, 8 Mar 2023 10:01:03 +0000 Subject: [PATCH 231/891] remove exchange rates --- .../customer-market/customer-market.js | 2 +- .../customer-stall/customer-stall.html | 72 +++++++-- .../customer-stall/customer-stall.js | 144 +++++++++++------- .../components/product-card/product-card.html | 3 - .../product-detail/product-detail.html | 3 - static/js/market.js | 25 +-- static/js/utils.js | 12 ++ templates/nostrmarket/market.html | 3 +- 8 files changed, 163 insertions(+), 101 deletions(-) diff --git a/static/components/customer-market/customer-market.js b/static/components/customer-market/customer-market.js index c14ffa9..45d5bea 100644 --- a/static/components/customer-market/customer-market.js +++ b/static/components/customer-market/customer-market.js @@ -4,7 +4,7 @@ async function customerMarket(path) { name: 'customer-market', template, - props: ['products', 'exchange-rates', 'change-page'], + props: ['products', 'change-page'], data: function () { return {} }, diff --git a/static/components/customer-stall/customer-stall.html b/static/components/customer-stall/customer-stall.html index c6ac229..ea63043 100644 --- a/static/components/customer-stall/customer-stall.html +++ b/static/components/customer-stall/customer-stall.html @@ -10,6 +10,12 @@ + @@ -64,12 +71,12 @@ filled dense readonly - hint="This your key pair! Don't lose it!" - v-if="checkoutDialog.data.privkey" - v-model="checkoutDialog.data.privkey" + type="password" + v-if="customerPrivkey" + v-model="customerPrivkey" > -
+
- Total: {{ stall.currency != 'sat' ? getAmountFormated(finalCost) : - finalCost + 'sats' }} - ({{ getValueInSats(finalCost) }} sats) + > -->
+ +
+ +
+
+ +
+
+ Copy invoice + Close +
+ +
diff --git a/static/components/customer-stall/customer-stall.js b/static/components/customer-stall/customer-stall.js index b72ba7d..4bd74c3 100644 --- a/static/components/customer-stall/customer-stall.js +++ b/static/components/customer-stall/customer-stall.js @@ -6,15 +6,16 @@ async function customerStall(path) { template, props: [ + 'account', 'stall', 'products', - 'exchange-rates', 'product-detail', 'change-page', 'relays' ], data: function () { return { + loading: false, cart: { total: 0, size: 0, @@ -23,8 +24,9 @@ async function customerStall(path) { cartMenu: [], hasNip07: false, customerPubkey: null, - customerPrivKey: null, - nostrMessages: new Map(), + customerPrivkey: null, + customerUseExtension: null, + activeOrder: null, checkoutDialog: { show: false, data: { @@ -58,12 +60,6 @@ async function customerStall(path) { changePageS(page, opts) { this.$emit('change-page', page, opts) }, - getValueInSats(amount, unit = 'USD') { - if (!this.exchangeRates) return 0 - return Math.ceil( - (amount / this.exchangeRates[`BTC${unit}`][unit]) * 1e8 - ) - }, getAmountFormated(amount, unit = 'USD') { return LNbits.utils.formatCurrency(amount, unit) }, @@ -125,26 +121,12 @@ async function customerStall(path) { } } }, - async getPubkey() { - try { - this.customerPubkey = await window.nostr.getPublicKey() - this.checkoutDialog.data.pubkey = this.customerPubkey - this.checkoutDialog.data.privkey = null - } catch (err) { - console.error( - `Failed to get a public key from a Nostr extension: ${err}` - ) - } - }, - generateKeyPair() { - let sk = NostrTools.generatePrivateKey() - let pk = NostrTools.getPublicKey(sk) - this.customerPubkey = pk - this.customerPrivKey = sk - this.checkoutDialog.data.pubkey = this.customerPubkey - this.checkoutDialog.data.privkey = this.customerPrivKey + closeQrCodeDialog() { + this.qrCodeDialog.dismissMsg() + this.qrCodeDialog.show = false }, async placeOrder() { + this.loading = true LNbits.utils .confirmDialog( `Send the order to the merchant? You should receive a message with the payment details.` @@ -170,6 +152,7 @@ async function customerStall(path) { ':' ) ) + this.activeOrder = orderObj.id let event = { ...(await NostrTools.getBlankEvent()), kind: 4, @@ -177,13 +160,13 @@ async function customerStall(path) { tags: [['p', this.stall.pubkey]], pubkey: this.customerPubkey } - if (this.customerPrivKey) { + if (this.customerPrivkey) { event.content = await NostrTools.nip04.encrypt( - this.customerPrivKey, + this.customerPrivkey, this.stall.pubkey, JSON.stringify(orderObj) ) - } else { + } else if (this.customerUseExtension && this.hasNip07) { event.content = await window.nostr.nip04.encrypt( this.stall.pubkey, JSON.stringify(orderObj) @@ -196,15 +179,15 @@ async function customerStall(path) { } } event.id = NostrTools.getEventHash(event) - if (this.customerPrivKey) { + if (this.customerPrivkey) { event.sig = await NostrTools.signEvent( event, - this.customerPrivKey + this.customerPrivkey ) - } else if (this.hasNip07) { + } else if (this.customerUseExtension && this.hasNip07) { event = await window.nostr.signEvent(event) } - console.log(event, orderObj) + await this.sendOrder(event) }) }, @@ -223,25 +206,32 @@ async function customerStall(path) { let pub = relay.publish(order) pub.on('ok', () => { console.log(`${relay.url} has accepted our event`) + relay.close() }) pub.on('failed', reason => { console.log(`failed to publish to ${relay.url}: ${reason}`) + relay.close() }) } catch (err) { console.error(`Error: ${err}`) } } + this.loading = false this.resetCheckout() + this.resetCart() + this.qrCodeDialog.show = true + this.qrCodeDialog.dismissMsg = this.$q.notify({ + timeout: 0, + message: 'Waiting for invoice from merchant...' + }) this.listenMessages() }, async listenMessages() { + console.log('LISTEN') try { const pool = new NostrTools.SimplePool() const filters = [ - { - kinds: [4], - authors: [this.customerPubkey] - }, + // / { kinds: [4], '#p': [this.customerPubkey] @@ -254,38 +244,74 @@ async function customerStall(path) { let sender = mine ? event.tags.find(([k, v]) => k === 'p' && v && v !== '')[1] : event.pubkey - if ( - (mine && sender != this.stall.pubkey) || - (!mine && sender != this.customerPubkey) - ) { - console.log(`Not relevant message!`) - return - } + try { - let plaintext = this.customerPrivKey - ? await NostrTools.nip04.decrypt( - this.customerPrivKey, - sender, - event.content - ) - : await window.nostr.nip04.decrypt(sender, event.content) - // console.log(`${mine ? 'Me' : 'Customer'}: ${plaintext}`) - this.nostrMessages.set(event.id, { - msg: plaintext, - timestamp: event.created_at, - sender: `${mine ? 'Me' : 'Merchant'}` - }) + let plaintext + if (this.customerPrivkey) { + plaintext = await NostrTools.nip04.decrypt( + this.customerPrivkey, + sender, + event.content + ) + } else if (this.customerUseExtension && this.hasNip07) { + plaintext = await window.nostr.nip04.decrypt( + sender, + event.content + ) + } + console.log(`${mine ? 'Me' : 'Merchant'}: ${plaintext}`) + + // this.nostrMessages.set(event.id, { + // msg: plaintext, + // timestamp: event.created_at, + // sender: `${mine ? 'Me' : 'Merchant'}` + // }) + this.messageFilter(plaintext, cb => Promise.resolve(pool.close)) } catch { console.error('Unable to decrypt message!') - return } }) } catch (err) { console.error(`Error: ${err}`) } + }, + messageFilter(text, cb = () => {}) { + if (!isJson(text)) return + let json = JSON.parse(text) + if (json.id != this.activeOrder) return + if (json?.payment_options) { + // this.qrCodeDialog.show = true + this.qrCodeDialog.data.payment_request = json.payment_options.find( + o => o.type == 'ln' + ).link + this.qrCodeDialog.dismissMsg = this.$q.notify({ + timeout: 0, + message: 'Waiting for payment...' + }) + } else if (json?.paid) { + this.qrCodeDialog.dismissMsg = this.$q.notify({ + type: 'positive', + message: 'Sats received, thanks!', + icon: 'thumb_up' + }) + this.closeQrCodeDialog() + this.activeOrder = null + Promise.resolve(cb()) + } else { + return + } } + // async mockInit() { + // this.customerPubkey = await window.nostr.getPublicKey() + // this.activeOrder = + // 'e4a16aa0198022dc682b2b52ed15767438282c0e712f510332fc047eaf795313' + // await this.listenMessages() + // } }, created() { + this.customerPubkey = this.account.pubkey + this.customerPrivkey = this.account.privkey + this.customerUseExtension = this.account.useExtension setTimeout(() => { if (window.nostr) { this.hasNip07 = true diff --git a/static/components/product-card/product-card.html b/static/components/product-card/product-card.html index 5a92ba3..95f86e2 100644 --- a/static/components/product-card/product-card.html +++ b/static/components/product-card/product-card.html @@ -43,9 +43,6 @@ {{ product.formatedPrice }} - ({{ product.priceInSats }} sats) {{ product.quantity }} left {{ product.formatedPrice }} - ({{ product.priceInSats }} sats) { key: null } }, - drawer: true, + drawer: false, pubkeys: new Set(), relays: new Set(), events: [], @@ -49,7 +49,6 @@ const market = async () => { products: [], profiles: new Map(), searchText: null, - exchangeRates: null, inputPubkey: null, inputRelay: null, activePage: 'market', @@ -136,10 +135,8 @@ const market = async () => { } // Get notes from Nostr - //await this.initNostr() + await this.initNostr() - // Get fiat rates (i think there's an LNbits endpoint for this) - //await this.getRates() this.$q.loading.hide() }, methods: { @@ -233,23 +230,12 @@ const market = async () => { obj.images = [obj.image] if (obj.currency != 'sat') { obj.formatedPrice = this.getAmountFormated(obj.price, obj.currency) - obj.priceInSats = this.getValueInSats(obj.price, obj.currency) } return obj }) pool.close(relays) return }, - async getRates() { - let noFiat = this.stalls.map(s => s.currency).every(c => c == 'sat') - if (noFiat) return - try { - let rates = await axios.get('https://api.opennode.co/v1/rates') - this.exchangeRates = rates.data.data - } catch (error) { - LNbits.utils.notifyApiError(error) - } - }, navigateTo(page, opts = {stall: null, product: null, pubkey: null}) { let {stall, product, pubkey} = opts let url = new URL(window.location) @@ -283,13 +269,6 @@ const market = async () => { window.history.pushState({}, '', url) this.activePage = page }, - - getValueInSats(amount, unit = 'USD') { - if (!this.exchangeRates) return 0 - return Math.ceil( - (amount / this.exchangeRates[`BTC${unit}`][unit]) * 1e8 - ) - }, getAmountFormated(amount, unit = 'USD') { return LNbits.utils.formatCurrency(amount, unit) }, diff --git a/static/js/utils.js b/static/js/utils.js index 3e3a14e..736fc8d 100644 --- a/static/js/utils.js +++ b/static/js/utils.js @@ -35,3 +35,15 @@ async function hash(string) { .join('') return hashHex } + +function isJson(str) { + if (typeof str !== 'string') { + return false + } + try { + JSON.parse(str) + return true + } catch (error) { + return false + } +} diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html index 77c0cac..20a4332 100644 --- a/templates/nostrmarket/market.html +++ b/templates/nostrmarket/market.html @@ -169,15 +169,14 @@ v-if="!isLoading && activeStall" :stall="stalls.find(stall => stall.id == activeStall)" :products="filterProducts" - :exchange-rates="exchangeRates" :product-detail="activeProduct" :relays="relays" + :account="account" @change-page="navigateTo" > From 68d36ae3a071fcb548a08a3ad7146729a940852a Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Wed, 8 Mar 2023 10:01:03 +0000 Subject: [PATCH 232/891] remove exchange rates --- .../customer-market/customer-market.js | 2 +- .../customer-stall/customer-stall.html | 72 +++++++-- .../customer-stall/customer-stall.js | 144 +++++++++++------- .../components/product-card/product-card.html | 3 - .../product-detail/product-detail.html | 3 - static/js/market.js | 25 +-- static/js/utils.js | 12 ++ templates/nostrmarket/market.html | 3 +- 8 files changed, 163 insertions(+), 101 deletions(-) diff --git a/static/components/customer-market/customer-market.js b/static/components/customer-market/customer-market.js index c14ffa9..45d5bea 100644 --- a/static/components/customer-market/customer-market.js +++ b/static/components/customer-market/customer-market.js @@ -4,7 +4,7 @@ async function customerMarket(path) { name: 'customer-market', template, - props: ['products', 'exchange-rates', 'change-page'], + props: ['products', 'change-page'], data: function () { return {} }, diff --git a/static/components/customer-stall/customer-stall.html b/static/components/customer-stall/customer-stall.html index c6ac229..ea63043 100644 --- a/static/components/customer-stall/customer-stall.html +++ b/static/components/customer-stall/customer-stall.html @@ -10,6 +10,12 @@ + @@ -64,12 +71,12 @@ filled dense readonly - hint="This your key pair! Don't lose it!" - v-if="checkoutDialog.data.privkey" - v-model="checkoutDialog.data.privkey" + type="password" + v-if="customerPrivkey" + v-model="customerPrivkey" > -
+
- Total: {{ stall.currency != 'sat' ? getAmountFormated(finalCost) : - finalCost + 'sats' }} - ({{ getValueInSats(finalCost) }} sats) + > -->
+ +
+ +
+
+ + +
+ Copy invoice + Close +
+
+
diff --git a/static/components/customer-stall/customer-stall.js b/static/components/customer-stall/customer-stall.js index b72ba7d..4bd74c3 100644 --- a/static/components/customer-stall/customer-stall.js +++ b/static/components/customer-stall/customer-stall.js @@ -6,15 +6,16 @@ async function customerStall(path) { template, props: [ + 'account', 'stall', 'products', - 'exchange-rates', 'product-detail', 'change-page', 'relays' ], data: function () { return { + loading: false, cart: { total: 0, size: 0, @@ -23,8 +24,9 @@ async function customerStall(path) { cartMenu: [], hasNip07: false, customerPubkey: null, - customerPrivKey: null, - nostrMessages: new Map(), + customerPrivkey: null, + customerUseExtension: null, + activeOrder: null, checkoutDialog: { show: false, data: { @@ -58,12 +60,6 @@ async function customerStall(path) { changePageS(page, opts) { this.$emit('change-page', page, opts) }, - getValueInSats(amount, unit = 'USD') { - if (!this.exchangeRates) return 0 - return Math.ceil( - (amount / this.exchangeRates[`BTC${unit}`][unit]) * 1e8 - ) - }, getAmountFormated(amount, unit = 'USD') { return LNbits.utils.formatCurrency(amount, unit) }, @@ -125,26 +121,12 @@ async function customerStall(path) { } } }, - async getPubkey() { - try { - this.customerPubkey = await window.nostr.getPublicKey() - this.checkoutDialog.data.pubkey = this.customerPubkey - this.checkoutDialog.data.privkey = null - } catch (err) { - console.error( - `Failed to get a public key from a Nostr extension: ${err}` - ) - } - }, - generateKeyPair() { - let sk = NostrTools.generatePrivateKey() - let pk = NostrTools.getPublicKey(sk) - this.customerPubkey = pk - this.customerPrivKey = sk - this.checkoutDialog.data.pubkey = this.customerPubkey - this.checkoutDialog.data.privkey = this.customerPrivKey + closeQrCodeDialog() { + this.qrCodeDialog.dismissMsg() + this.qrCodeDialog.show = false }, async placeOrder() { + this.loading = true LNbits.utils .confirmDialog( `Send the order to the merchant? You should receive a message with the payment details.` @@ -170,6 +152,7 @@ async function customerStall(path) { ':' ) ) + this.activeOrder = orderObj.id let event = { ...(await NostrTools.getBlankEvent()), kind: 4, @@ -177,13 +160,13 @@ async function customerStall(path) { tags: [['p', this.stall.pubkey]], pubkey: this.customerPubkey } - if (this.customerPrivKey) { + if (this.customerPrivkey) { event.content = await NostrTools.nip04.encrypt( - this.customerPrivKey, + this.customerPrivkey, this.stall.pubkey, JSON.stringify(orderObj) ) - } else { + } else if (this.customerUseExtension && this.hasNip07) { event.content = await window.nostr.nip04.encrypt( this.stall.pubkey, JSON.stringify(orderObj) @@ -196,15 +179,15 @@ async function customerStall(path) { } } event.id = NostrTools.getEventHash(event) - if (this.customerPrivKey) { + if (this.customerPrivkey) { event.sig = await NostrTools.signEvent( event, - this.customerPrivKey + this.customerPrivkey ) - } else if (this.hasNip07) { + } else if (this.customerUseExtension && this.hasNip07) { event = await window.nostr.signEvent(event) } - console.log(event, orderObj) + await this.sendOrder(event) }) }, @@ -223,25 +206,32 @@ async function customerStall(path) { let pub = relay.publish(order) pub.on('ok', () => { console.log(`${relay.url} has accepted our event`) + relay.close() }) pub.on('failed', reason => { console.log(`failed to publish to ${relay.url}: ${reason}`) + relay.close() }) } catch (err) { console.error(`Error: ${err}`) } } + this.loading = false this.resetCheckout() + this.resetCart() + this.qrCodeDialog.show = true + this.qrCodeDialog.dismissMsg = this.$q.notify({ + timeout: 0, + message: 'Waiting for invoice from merchant...' + }) this.listenMessages() }, async listenMessages() { + console.log('LISTEN') try { const pool = new NostrTools.SimplePool() const filters = [ - { - kinds: [4], - authors: [this.customerPubkey] - }, + // / { kinds: [4], '#p': [this.customerPubkey] @@ -254,38 +244,74 @@ async function customerStall(path) { let sender = mine ? event.tags.find(([k, v]) => k === 'p' && v && v !== '')[1] : event.pubkey - if ( - (mine && sender != this.stall.pubkey) || - (!mine && sender != this.customerPubkey) - ) { - console.log(`Not relevant message!`) - return - } + try { - let plaintext = this.customerPrivKey - ? await NostrTools.nip04.decrypt( - this.customerPrivKey, - sender, - event.content - ) - : await window.nostr.nip04.decrypt(sender, event.content) - // console.log(`${mine ? 'Me' : 'Customer'}: ${plaintext}`) - this.nostrMessages.set(event.id, { - msg: plaintext, - timestamp: event.created_at, - sender: `${mine ? 'Me' : 'Merchant'}` - }) + let plaintext + if (this.customerPrivkey) { + plaintext = await NostrTools.nip04.decrypt( + this.customerPrivkey, + sender, + event.content + ) + } else if (this.customerUseExtension && this.hasNip07) { + plaintext = await window.nostr.nip04.decrypt( + sender, + event.content + ) + } + console.log(`${mine ? 'Me' : 'Merchant'}: ${plaintext}`) + + // this.nostrMessages.set(event.id, { + // msg: plaintext, + // timestamp: event.created_at, + // sender: `${mine ? 'Me' : 'Merchant'}` + // }) + this.messageFilter(plaintext, cb => Promise.resolve(pool.close)) } catch { console.error('Unable to decrypt message!') - return } }) } catch (err) { console.error(`Error: ${err}`) } + }, + messageFilter(text, cb = () => {}) { + if (!isJson(text)) return + let json = JSON.parse(text) + if (json.id != this.activeOrder) return + if (json?.payment_options) { + // this.qrCodeDialog.show = true + this.qrCodeDialog.data.payment_request = json.payment_options.find( + o => o.type == 'ln' + ).link + this.qrCodeDialog.dismissMsg = this.$q.notify({ + timeout: 0, + message: 'Waiting for payment...' + }) + } else if (json?.paid) { + this.qrCodeDialog.dismissMsg = this.$q.notify({ + type: 'positive', + message: 'Sats received, thanks!', + icon: 'thumb_up' + }) + this.closeQrCodeDialog() + this.activeOrder = null + Promise.resolve(cb()) + } else { + return + } } + // async mockInit() { + // this.customerPubkey = await window.nostr.getPublicKey() + // this.activeOrder = + // 'e4a16aa0198022dc682b2b52ed15767438282c0e712f510332fc047eaf795313' + // await this.listenMessages() + // } }, created() { + this.customerPubkey = this.account.pubkey + this.customerPrivkey = this.account.privkey + this.customerUseExtension = this.account.useExtension setTimeout(() => { if (window.nostr) { this.hasNip07 = true diff --git a/static/components/product-card/product-card.html b/static/components/product-card/product-card.html index 5a92ba3..95f86e2 100644 --- a/static/components/product-card/product-card.html +++ b/static/components/product-card/product-card.html @@ -43,9 +43,6 @@
{{ product.formatedPrice }} - ({{ product.priceInSats }} sats) {{ product.quantity }} left {{ product.formatedPrice }} - ({{ product.priceInSats }} sats) { key: null } }, - drawer: true, + drawer: false, pubkeys: new Set(), relays: new Set(), events: [], @@ -49,7 +49,6 @@ const market = async () => { products: [], profiles: new Map(), searchText: null, - exchangeRates: null, inputPubkey: null, inputRelay: null, activePage: 'market', @@ -136,10 +135,8 @@ const market = async () => { } // Get notes from Nostr - //await this.initNostr() + await this.initNostr() - // Get fiat rates (i think there's an LNbits endpoint for this) - //await this.getRates() this.$q.loading.hide() }, methods: { @@ -233,23 +230,12 @@ const market = async () => { obj.images = [obj.image] if (obj.currency != 'sat') { obj.formatedPrice = this.getAmountFormated(obj.price, obj.currency) - obj.priceInSats = this.getValueInSats(obj.price, obj.currency) } return obj }) pool.close(relays) return }, - async getRates() { - let noFiat = this.stalls.map(s => s.currency).every(c => c == 'sat') - if (noFiat) return - try { - let rates = await axios.get('https://api.opennode.co/v1/rates') - this.exchangeRates = rates.data.data - } catch (error) { - LNbits.utils.notifyApiError(error) - } - }, navigateTo(page, opts = {stall: null, product: null, pubkey: null}) { let {stall, product, pubkey} = opts let url = new URL(window.location) @@ -283,13 +269,6 @@ const market = async () => { window.history.pushState({}, '', url) this.activePage = page }, - - getValueInSats(amount, unit = 'USD') { - if (!this.exchangeRates) return 0 - return Math.ceil( - (amount / this.exchangeRates[`BTC${unit}`][unit]) * 1e8 - ) - }, getAmountFormated(amount, unit = 'USD') { return LNbits.utils.formatCurrency(amount, unit) }, diff --git a/static/js/utils.js b/static/js/utils.js index 3e3a14e..736fc8d 100644 --- a/static/js/utils.js +++ b/static/js/utils.js @@ -35,3 +35,15 @@ async function hash(string) { .join('') return hashHex } + +function isJson(str) { + if (typeof str !== 'string') { + return false + } + try { + JSON.parse(str) + return true + } catch (error) { + return false + } +} diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html index 77c0cac..20a4332 100644 --- a/templates/nostrmarket/market.html +++ b/templates/nostrmarket/market.html @@ -169,15 +169,14 @@ v-if="!isLoading && activeStall" :stall="stalls.find(stall => stall.id == activeStall)" :products="filterProducts" - :exchange-rates="exchangeRates" :product-detail="activeProduct" :relays="relays" + :account="account" @change-page="navigateTo" > From d4b483cb19b1e3cfab2481724262ce70b63c340c Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Wed, 8 Mar 2023 10:01:15 +0000 Subject: [PATCH 233/891] chat dialog component --- .../components/chat-dialog/chat-dialog.html | 89 +++++++++++ static/components/chat-dialog/chat-dialog.js | 143 ++++++++++++++++++ 2 files changed, 232 insertions(+) create mode 100644 static/components/chat-dialog/chat-dialog.html create mode 100644 static/components/chat-dialog/chat-dialog.js diff --git a/static/components/chat-dialog/chat-dialog.html b/static/components/chat-dialog/chat-dialog.html new file mode 100644 index 0000000..9ab5296 --- /dev/null +++ b/static/components/chat-dialog/chat-dialog.html @@ -0,0 +1,89 @@ +
+ + + + + +
Chat Box
+ + + + Minimize + + + Maximize + + + Close + +
+ + +
+
+ +
+ +
+
+ + + + + + + +
+
+
+
+
diff --git a/static/components/chat-dialog/chat-dialog.js b/static/components/chat-dialog/chat-dialog.js new file mode 100644 index 0000000..96f82f6 --- /dev/null +++ b/static/components/chat-dialog/chat-dialog.js @@ -0,0 +1,143 @@ +async function chatDialog(path) { + const template = await loadTemplateAsync(path) + + Vue.component('chat-dialog', { + name: 'chat-dialog', + template, + + props: ['account', 'merchant', 'relays'], + data: function () { + return { + dialog: false, + maximizedToggle: true, + pool: null, + nostrMessages: [], + newMessage: '' + } + }, + computed: { + sortedMessages() { + return this.nostrMessages.sort((a, b) => a.timestamp - b.timestamp) + } + }, + methods: { + async startPool() { + let sub = this.pool.sub(Array.from(this.relays), [ + { + kinds: [4], + authors: [this.account.pubkey] + }, + { + kinds: [4], + '#p': [this.account.pubkey] + } + ]) + sub.on('event', async event => { + let mine = event.pubkey == this.account.pubkey + let sender = mine + ? event.tags.find(([k, v]) => k === 'p' && v && v !== '')[1] + : event.pubkey + + try { + let plaintext + if (this.account.privkey) { + plaintext = await NostrTools.nip04.decrypt( + this.account.privkey, + sender, + event.content + ) + } else if (this.account.useExtension && this.hasNip07) { + plaintext = await window.nostr.nip04.decrypt( + sender, + event.content + ) + } + this.nostrMessages.push({ + id: event.id, + msg: plaintext, + timestamp: event.created_at, + sender: `${mine ? 'Me' : 'Merchant'}` + }) + } catch { + console.error('Unable to decrypt message!') + } + }) + }, + async sendMessage() { + if (this.newMessage && this.newMessage.length < 1) return + let event = { + ...(await NostrTools.getBlankEvent()), + kind: 4, + created_at: Math.floor(Date.now() / 1000), + tags: [['p', this.merchant]], + pubkey: this.account.pubkey, + content: await this.encryptMsg() + } + event.id = NostrTools.getEventHash(event) + event.sig = this.signEvent(event) + for (const url of Array.from(this.relays)) { + try { + let relay = NostrTools.relayInit(url) + relay.on('connect', () => { + console.debug(`connected to ${relay.url}`) + }) + relay.on('error', () => { + console.debug(`failed to connect to ${relay.url}`) + }) + + await relay.connect() + let pub = relay.publish(event) + pub.on('ok', () => { + console.debug(`${relay.url} has accepted our event`) + relay.close() + }) + pub.on('failed', reason => { + console.debug(`failed to publish to ${relay.url}: ${reason}`) + relay.close() + }) + this.newMessage = '' + } catch (e) { + console.error(e) + } + } + }, + async encryptMsg() { + try { + let cypher + if (this.account.privkey) { + cypher = await NostrTools.nip04.encrypt( + this.account.privkey, + this.merchant, + this.newMessage + ) + } else if (this.account.useExtension && this.hasNip07) { + cypher = await window.nostr.nip04.encrypt( + this.merchant, + this.newMessage + ) + } + return cypher + } catch (e) { + console.error(e) + } + }, + async signEvent(event) { + if (this.account.privkey) { + event.sig = await NostrTools.signEvent(event, this.account.privkey) + } else if (this.account.useExtension && this.hasNip07) { + event = await window.nostr.signEvent(event) + } + return event + } + }, + created() { + this.pool = new NostrTools.SimplePool() + setTimeout(() => { + if (window.nostr) { + this.hasNip07 = true + } + }, 1000) + this.startPool() + } + }) +} From 1e9b138297f41792fa65d76cef674b8ec9adb544 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Wed, 8 Mar 2023 10:01:15 +0000 Subject: [PATCH 234/891] chat dialog component --- .../components/chat-dialog/chat-dialog.html | 89 +++++++++++ static/components/chat-dialog/chat-dialog.js | 143 ++++++++++++++++++ 2 files changed, 232 insertions(+) create mode 100644 static/components/chat-dialog/chat-dialog.html create mode 100644 static/components/chat-dialog/chat-dialog.js diff --git a/static/components/chat-dialog/chat-dialog.html b/static/components/chat-dialog/chat-dialog.html new file mode 100644 index 0000000..9ab5296 --- /dev/null +++ b/static/components/chat-dialog/chat-dialog.html @@ -0,0 +1,89 @@ +
+ + + + + +
Chat Box
+ + + + Minimize + + + Maximize + + + Close + +
+ + +
+
+ +
+ +
+
+ + + + + + + +
+
+
+
+
diff --git a/static/components/chat-dialog/chat-dialog.js b/static/components/chat-dialog/chat-dialog.js new file mode 100644 index 0000000..96f82f6 --- /dev/null +++ b/static/components/chat-dialog/chat-dialog.js @@ -0,0 +1,143 @@ +async function chatDialog(path) { + const template = await loadTemplateAsync(path) + + Vue.component('chat-dialog', { + name: 'chat-dialog', + template, + + props: ['account', 'merchant', 'relays'], + data: function () { + return { + dialog: false, + maximizedToggle: true, + pool: null, + nostrMessages: [], + newMessage: '' + } + }, + computed: { + sortedMessages() { + return this.nostrMessages.sort((a, b) => a.timestamp - b.timestamp) + } + }, + methods: { + async startPool() { + let sub = this.pool.sub(Array.from(this.relays), [ + { + kinds: [4], + authors: [this.account.pubkey] + }, + { + kinds: [4], + '#p': [this.account.pubkey] + } + ]) + sub.on('event', async event => { + let mine = event.pubkey == this.account.pubkey + let sender = mine + ? event.tags.find(([k, v]) => k === 'p' && v && v !== '')[1] + : event.pubkey + + try { + let plaintext + if (this.account.privkey) { + plaintext = await NostrTools.nip04.decrypt( + this.account.privkey, + sender, + event.content + ) + } else if (this.account.useExtension && this.hasNip07) { + plaintext = await window.nostr.nip04.decrypt( + sender, + event.content + ) + } + this.nostrMessages.push({ + id: event.id, + msg: plaintext, + timestamp: event.created_at, + sender: `${mine ? 'Me' : 'Merchant'}` + }) + } catch { + console.error('Unable to decrypt message!') + } + }) + }, + async sendMessage() { + if (this.newMessage && this.newMessage.length < 1) return + let event = { + ...(await NostrTools.getBlankEvent()), + kind: 4, + created_at: Math.floor(Date.now() / 1000), + tags: [['p', this.merchant]], + pubkey: this.account.pubkey, + content: await this.encryptMsg() + } + event.id = NostrTools.getEventHash(event) + event.sig = this.signEvent(event) + for (const url of Array.from(this.relays)) { + try { + let relay = NostrTools.relayInit(url) + relay.on('connect', () => { + console.debug(`connected to ${relay.url}`) + }) + relay.on('error', () => { + console.debug(`failed to connect to ${relay.url}`) + }) + + await relay.connect() + let pub = relay.publish(event) + pub.on('ok', () => { + console.debug(`${relay.url} has accepted our event`) + relay.close() + }) + pub.on('failed', reason => { + console.debug(`failed to publish to ${relay.url}: ${reason}`) + relay.close() + }) + this.newMessage = '' + } catch (e) { + console.error(e) + } + } + }, + async encryptMsg() { + try { + let cypher + if (this.account.privkey) { + cypher = await NostrTools.nip04.encrypt( + this.account.privkey, + this.merchant, + this.newMessage + ) + } else if (this.account.useExtension && this.hasNip07) { + cypher = await window.nostr.nip04.encrypt( + this.merchant, + this.newMessage + ) + } + return cypher + } catch (e) { + console.error(e) + } + }, + async signEvent(event) { + if (this.account.privkey) { + event.sig = await NostrTools.signEvent(event, this.account.privkey) + } else if (this.account.useExtension && this.hasNip07) { + event = await window.nostr.signEvent(event) + } + return event + } + }, + created() { + this.pool = new NostrTools.SimplePool() + setTimeout(() => { + if (window.nostr) { + this.hasNip07 = true + } + }, 1000) + this.startPool() + } + }) +} From 4817e6a3ea43b9cef73e56291285f482f4111901 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Wed, 8 Mar 2023 11:23:51 +0000 Subject: [PATCH 235/891] clean up --- static/components/customer-stall/customer-stall.html | 3 --- 1 file changed, 3 deletions(-) diff --git a/static/components/customer-stall/customer-stall.html b/static/components/customer-stall/customer-stall.html index ea63043..5e2d085 100644 --- a/static/components/customer-stall/customer-stall.html +++ b/static/components/customer-stall/customer-stall.html @@ -121,9 +121,6 @@
Total: {{ stall.currency != 'sat' ? getAmountFormated(finalCost, stall.currency) : finalCost + 'sats' }} -
Date: Wed, 8 Mar 2023 11:23:51 +0000 Subject: [PATCH 236/891] clean up --- static/components/customer-stall/customer-stall.html | 3 --- 1 file changed, 3 deletions(-) diff --git a/static/components/customer-stall/customer-stall.html b/static/components/customer-stall/customer-stall.html index ea63043..5e2d085 100644 --- a/static/components/customer-stall/customer-stall.html +++ b/static/components/customer-stall/customer-stall.html @@ -121,9 +121,6 @@
Total: {{ stall.currency != 'sat' ? getAmountFormated(finalCost, stall.currency) : finalCost + 'sats' }} -
Date: Wed, 8 Mar 2023 11:24:11 +0000 Subject: [PATCH 237/891] fix add merchant pubkey --- static/js/market.js | 6 +++++- templates/nostrmarket/market.html | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/static/js/market.js b/static/js/market.js index 0077344..0d823e9 100644 --- a/static/js/market.js +++ b/static/js/market.js @@ -222,6 +222,7 @@ const market = async () => { }) }) await Promise.resolve(sub) + this.$q.loading.hide() this.stalls = await Array.from(stalls.values()) this.products = Array.from(products.values()).map(obj => { @@ -272,7 +273,8 @@ const market = async () => { getAmountFormated(amount, unit = 'USD') { return LNbits.utils.formatCurrency(amount, unit) }, - async addPubkey(pubkey = null) { + async addPubkey(pubkey) { + console.log(pubkey, this.inputPubkey) if (!pubkey) { pubkey = String(this.inputPubkey).trim() } @@ -285,6 +287,7 @@ const market = async () => { pubkey = data.pubkey givenRelays = data.relays } + console.log(pubkey) this.pubkeys.add(pubkey) this.inputPubkey = null } catch (err) { @@ -310,6 +313,7 @@ const market = async () => { `diagonAlley.merchants`, Array.from(this.pubkeys) ) + Promise.resolve(this.initNostr()) }, async addRelay() { let relay = String(this.inputRelay).trim() diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html index 20a4332..a5ebeb3 100644 --- a/templates/nostrmarket/market.html +++ b/templates/nostrmarket/market.html @@ -44,12 +44,12 @@ - + From 46db24855259c378792d365cb4da4768b3f743d1 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Wed, 8 Mar 2023 11:24:11 +0000 Subject: [PATCH 238/891] fix add merchant pubkey --- static/js/market.js | 6 +++++- templates/nostrmarket/market.html | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/static/js/market.js b/static/js/market.js index 0077344..0d823e9 100644 --- a/static/js/market.js +++ b/static/js/market.js @@ -222,6 +222,7 @@ const market = async () => { }) }) await Promise.resolve(sub) + this.$q.loading.hide() this.stalls = await Array.from(stalls.values()) this.products = Array.from(products.values()).map(obj => { @@ -272,7 +273,8 @@ const market = async () => { getAmountFormated(amount, unit = 'USD') { return LNbits.utils.formatCurrency(amount, unit) }, - async addPubkey(pubkey = null) { + async addPubkey(pubkey) { + console.log(pubkey, this.inputPubkey) if (!pubkey) { pubkey = String(this.inputPubkey).trim() } @@ -285,6 +287,7 @@ const market = async () => { pubkey = data.pubkey givenRelays = data.relays } + console.log(pubkey) this.pubkeys.add(pubkey) this.inputPubkey = null } catch (err) { @@ -310,6 +313,7 @@ const market = async () => { `diagonAlley.merchants`, Array.from(this.pubkeys) ) + Promise.resolve(this.initNostr()) }, async addRelay() { let relay = String(this.inputRelay).trim() diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html index 20a4332..a5ebeb3 100644 --- a/templates/nostrmarket/market.html +++ b/templates/nostrmarket/market.html @@ -44,12 +44,12 @@ - + From 8042f1235799a49db150542f6aac887e8a88fe29 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Wed, 8 Mar 2023 15:56:39 +0000 Subject: [PATCH 239/891] better-ish chat box --- .../components/chat-dialog/chat-dialog.html | 118 ++++++++---------- static/components/chat-dialog/chat-dialog.js | 81 +++++++++++- 2 files changed, 124 insertions(+), 75 deletions(-) diff --git a/static/components/chat-dialog/chat-dialog.html b/static/components/chat-dialog/chat-dialog.html index 9ab5296..fd87273 100644 --- a/static/components/chat-dialog/chat-dialog.html +++ b/static/components/chat-dialog/chat-dialog.html @@ -1,9 +1,9 @@ -
- +
+ @@ -13,77 +13,57 @@
Chat Box
- - Minimize - - - Maximize - - + + Close - -
-
- -
- -
-
- - - - - - - -
+ + + + + + + + + + + +
diff --git a/static/components/chat-dialog/chat-dialog.js b/static/components/chat-dialog/chat-dialog.js index 96f82f6..e89e26d 100644 --- a/static/components/chat-dialog/chat-dialog.js +++ b/static/components/chat-dialog/chat-dialog.js @@ -9,7 +9,7 @@ async function chatDialog(path) { data: function () { return { dialog: false, - maximizedToggle: true, + loading: false, pool: null, nostrMessages: [], newMessage: '' @@ -17,11 +17,22 @@ async function chatDialog(path) { }, computed: { sortedMessages() { - return this.nostrMessages.sort((a, b) => a.timestamp - b.timestamp) + return this.nostrMessages.sort((a, b) => b.timestamp - a.timestamp) } }, methods: { + async startDialog() { + this.dialog = true + await this.startPool() + }, + async closeDialog() { + this.dialog = false + await this.pool.close(Array.from(this.relays)) + }, async startPool() { + this.loading = true + this.pool = new NostrTools.SimplePool() + let messagesMap = new Map() let sub = this.pool.sub(Array.from(this.relays), [ { kinds: [4], @@ -32,6 +43,10 @@ async function chatDialog(path) { '#p': [this.account.pubkey] } ]) + sub.on('eose', () => { + this.loading = false + this.nostrMessages = Array.from(messagesMap.values()) + }) sub.on('event', async event => { let mine = event.pubkey == this.account.pubkey let sender = mine @@ -52,8 +67,7 @@ async function chatDialog(path) { event.content ) } - this.nostrMessages.push({ - id: event.id, + messagesMap.set(event.id, { msg: plaintext, timestamp: event.created_at, sender: `${mine ? 'Me' : 'Merchant'}` @@ -62,6 +76,10 @@ async function chatDialog(path) { console.error('Unable to decrypt message!') } }) + setTimeout(() => { + this.nostrMessages = Array.from(messagesMap.values()) + this.loading = false + }, 5000) }, async sendMessage() { if (this.newMessage && this.newMessage.length < 1) return @@ -128,16 +146,67 @@ async function chatDialog(path) { event = await window.nostr.signEvent(event) } return event + }, + timeFromNow(time) { + // Get timestamps + let unixTime = new Date(time).getTime() + if (!unixTime) return + let now = new Date().getTime() + + // Calculate difference + let difference = unixTime / 1000 - now / 1000 + + // Setup return object + let tfn = {} + + // Check if time is in the past, present, or future + tfn.when = 'now' + if (difference > 0) { + tfn.when = 'future' + } else if (difference < -1) { + tfn.when = 'past' + } + + // Convert difference to absolute + difference = Math.abs(difference) + + // Calculate time unit + if (difference / (60 * 60 * 24 * 365) > 1) { + // Years + tfn.unitOfTime = 'years' + tfn.time = Math.floor(difference / (60 * 60 * 24 * 365)) + } else if (difference / (60 * 60 * 24 * 45) > 1) { + // Months + tfn.unitOfTime = 'months' + tfn.time = Math.floor(difference / (60 * 60 * 24 * 45)) + } else if (difference / (60 * 60 * 24) > 1) { + // Days + tfn.unitOfTime = 'days' + tfn.time = Math.floor(difference / (60 * 60 * 24)) + } else if (difference / (60 * 60) > 1) { + // Hours + tfn.unitOfTime = 'hours' + tfn.time = Math.floor(difference / (60 * 60)) + } else if (difference / 60 > 1) { + // Minutes + tfn.unitOfTime = 'minutes' + tfn.time = Math.floor(difference / 60) + } else { + // Seconds + tfn.unitOfTime = 'seconds' + tfn.time = Math.floor(difference) + } + + // Return time from now data + return `${tfn.time} ${tfn.unitOfTime}` } }, created() { - this.pool = new NostrTools.SimplePool() setTimeout(() => { if (window.nostr) { this.hasNip07 = true } }, 1000) - this.startPool() } }) } From 2a652643c47d1e6608b76752c1ff607b078e35c2 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Wed, 8 Mar 2023 15:56:39 +0000 Subject: [PATCH 240/891] better-ish chat box --- .../components/chat-dialog/chat-dialog.html | 118 ++++++++---------- static/components/chat-dialog/chat-dialog.js | 81 +++++++++++- 2 files changed, 124 insertions(+), 75 deletions(-) diff --git a/static/components/chat-dialog/chat-dialog.html b/static/components/chat-dialog/chat-dialog.html index 9ab5296..fd87273 100644 --- a/static/components/chat-dialog/chat-dialog.html +++ b/static/components/chat-dialog/chat-dialog.html @@ -1,9 +1,9 @@ -
- +
+ @@ -13,77 +13,57 @@
Chat Box
- - Minimize - - - Maximize - - + + Close - -
-
- -
- -
-
- - - - - - - -
+ + + + + + + + + + + +
diff --git a/static/components/chat-dialog/chat-dialog.js b/static/components/chat-dialog/chat-dialog.js index 96f82f6..e89e26d 100644 --- a/static/components/chat-dialog/chat-dialog.js +++ b/static/components/chat-dialog/chat-dialog.js @@ -9,7 +9,7 @@ async function chatDialog(path) { data: function () { return { dialog: false, - maximizedToggle: true, + loading: false, pool: null, nostrMessages: [], newMessage: '' @@ -17,11 +17,22 @@ async function chatDialog(path) { }, computed: { sortedMessages() { - return this.nostrMessages.sort((a, b) => a.timestamp - b.timestamp) + return this.nostrMessages.sort((a, b) => b.timestamp - a.timestamp) } }, methods: { + async startDialog() { + this.dialog = true + await this.startPool() + }, + async closeDialog() { + this.dialog = false + await this.pool.close(Array.from(this.relays)) + }, async startPool() { + this.loading = true + this.pool = new NostrTools.SimplePool() + let messagesMap = new Map() let sub = this.pool.sub(Array.from(this.relays), [ { kinds: [4], @@ -32,6 +43,10 @@ async function chatDialog(path) { '#p': [this.account.pubkey] } ]) + sub.on('eose', () => { + this.loading = false + this.nostrMessages = Array.from(messagesMap.values()) + }) sub.on('event', async event => { let mine = event.pubkey == this.account.pubkey let sender = mine @@ -52,8 +67,7 @@ async function chatDialog(path) { event.content ) } - this.nostrMessages.push({ - id: event.id, + messagesMap.set(event.id, { msg: plaintext, timestamp: event.created_at, sender: `${mine ? 'Me' : 'Merchant'}` @@ -62,6 +76,10 @@ async function chatDialog(path) { console.error('Unable to decrypt message!') } }) + setTimeout(() => { + this.nostrMessages = Array.from(messagesMap.values()) + this.loading = false + }, 5000) }, async sendMessage() { if (this.newMessage && this.newMessage.length < 1) return @@ -128,16 +146,67 @@ async function chatDialog(path) { event = await window.nostr.signEvent(event) } return event + }, + timeFromNow(time) { + // Get timestamps + let unixTime = new Date(time).getTime() + if (!unixTime) return + let now = new Date().getTime() + + // Calculate difference + let difference = unixTime / 1000 - now / 1000 + + // Setup return object + let tfn = {} + + // Check if time is in the past, present, or future + tfn.when = 'now' + if (difference > 0) { + tfn.when = 'future' + } else if (difference < -1) { + tfn.when = 'past' + } + + // Convert difference to absolute + difference = Math.abs(difference) + + // Calculate time unit + if (difference / (60 * 60 * 24 * 365) > 1) { + // Years + tfn.unitOfTime = 'years' + tfn.time = Math.floor(difference / (60 * 60 * 24 * 365)) + } else if (difference / (60 * 60 * 24 * 45) > 1) { + // Months + tfn.unitOfTime = 'months' + tfn.time = Math.floor(difference / (60 * 60 * 24 * 45)) + } else if (difference / (60 * 60 * 24) > 1) { + // Days + tfn.unitOfTime = 'days' + tfn.time = Math.floor(difference / (60 * 60 * 24)) + } else if (difference / (60 * 60) > 1) { + // Hours + tfn.unitOfTime = 'hours' + tfn.time = Math.floor(difference / (60 * 60)) + } else if (difference / 60 > 1) { + // Minutes + tfn.unitOfTime = 'minutes' + tfn.time = Math.floor(difference / 60) + } else { + // Seconds + tfn.unitOfTime = 'seconds' + tfn.time = Math.floor(difference) + } + + // Return time from now data + return `${tfn.time} ${tfn.unitOfTime}` } }, created() { - this.pool = new NostrTools.SimplePool() setTimeout(() => { if (window.nostr) { this.hasNip07 = true } }, 1000) - this.startPool() } }) } From 5c4bcdf131ded925d7f3b02f5ebc8b8a80ac04b6 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Wed, 8 Mar 2023 15:57:16 +0000 Subject: [PATCH 241/891] payment dialog flow --- .../customer-stall/customer-stall.html | 16 +++++++--------- .../components/customer-stall/customer-stall.js | 17 ++--------------- 2 files changed, 9 insertions(+), 24 deletions(-) diff --git a/static/components/customer-stall/customer-stall.html b/static/components/customer-stall/customer-stall.html index 5e2d085..1e06b29 100644 --- a/static/components/customer-stall/customer-stall.html +++ b/static/components/customer-stall/customer-stall.html @@ -7,7 +7,10 @@ @click="$emit('change-page', 'market')" style="cursor: pointer" > - + - -
- -
-
+ + +
diff --git a/static/components/customer-stall/customer-stall.js b/static/components/customer-stall/customer-stall.js index 4bd74c3..58ab910 100644 --- a/static/components/customer-stall/customer-stall.js +++ b/static/components/customer-stall/customer-stall.js @@ -231,7 +231,6 @@ async function customerStall(path) { try { const pool = new NostrTools.SimplePool() const filters = [ - // / { kinds: [4], '#p': [this.customerPubkey] @@ -261,11 +260,6 @@ async function customerStall(path) { } console.log(`${mine ? 'Me' : 'Merchant'}: ${plaintext}`) - // this.nostrMessages.set(event.id, { - // msg: plaintext, - // timestamp: event.created_at, - // sender: `${mine ? 'Me' : 'Merchant'}` - // }) this.messageFilter(plaintext, cb => Promise.resolve(pool.close)) } catch { console.error('Unable to decrypt message!') @@ -280,7 +274,6 @@ async function customerStall(path) { let json = JSON.parse(text) if (json.id != this.activeOrder) return if (json?.payment_options) { - // this.qrCodeDialog.show = true this.qrCodeDialog.data.payment_request = json.payment_options.find( o => o.type == 'ln' ).link @@ -289,24 +282,18 @@ async function customerStall(path) { message: 'Waiting for payment...' }) } else if (json?.paid) { - this.qrCodeDialog.dismissMsg = this.$q.notify({ + this.closeQrCodeDialog() + this.$q.notify({ type: 'positive', message: 'Sats received, thanks!', icon: 'thumb_up' }) - this.closeQrCodeDialog() this.activeOrder = null Promise.resolve(cb()) } else { return } } - // async mockInit() { - // this.customerPubkey = await window.nostr.getPublicKey() - // this.activeOrder = - // 'e4a16aa0198022dc682b2b52ed15767438282c0e712f510332fc047eaf795313' - // await this.listenMessages() - // } }, created() { this.customerPubkey = this.account.pubkey From 9f1d11d4518a933bca8736950253cc7176b2a497 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Wed, 8 Mar 2023 15:57:16 +0000 Subject: [PATCH 242/891] payment dialog flow --- .../customer-stall/customer-stall.html | 16 +++++++--------- .../components/customer-stall/customer-stall.js | 17 ++--------------- 2 files changed, 9 insertions(+), 24 deletions(-) diff --git a/static/components/customer-stall/customer-stall.html b/static/components/customer-stall/customer-stall.html index 5e2d085..1e06b29 100644 --- a/static/components/customer-stall/customer-stall.html +++ b/static/components/customer-stall/customer-stall.html @@ -7,7 +7,10 @@ @click="$emit('change-page', 'market')" style="cursor: pointer" > - + - -
- -
-
+ + +
diff --git a/static/components/customer-stall/customer-stall.js b/static/components/customer-stall/customer-stall.js index 4bd74c3..58ab910 100644 --- a/static/components/customer-stall/customer-stall.js +++ b/static/components/customer-stall/customer-stall.js @@ -231,7 +231,6 @@ async function customerStall(path) { try { const pool = new NostrTools.SimplePool() const filters = [ - // / { kinds: [4], '#p': [this.customerPubkey] @@ -261,11 +260,6 @@ async function customerStall(path) { } console.log(`${mine ? 'Me' : 'Merchant'}: ${plaintext}`) - // this.nostrMessages.set(event.id, { - // msg: plaintext, - // timestamp: event.created_at, - // sender: `${mine ? 'Me' : 'Merchant'}` - // }) this.messageFilter(plaintext, cb => Promise.resolve(pool.close)) } catch { console.error('Unable to decrypt message!') @@ -280,7 +274,6 @@ async function customerStall(path) { let json = JSON.parse(text) if (json.id != this.activeOrder) return if (json?.payment_options) { - // this.qrCodeDialog.show = true this.qrCodeDialog.data.payment_request = json.payment_options.find( o => o.type == 'ln' ).link @@ -289,24 +282,18 @@ async function customerStall(path) { message: 'Waiting for payment...' }) } else if (json?.paid) { - this.qrCodeDialog.dismissMsg = this.$q.notify({ + this.closeQrCodeDialog() + this.$q.notify({ type: 'positive', message: 'Sats received, thanks!', icon: 'thumb_up' }) - this.closeQrCodeDialog() this.activeOrder = null Promise.resolve(cb()) } else { return } } - // async mockInit() { - // this.customerPubkey = await window.nostr.getPublicKey() - // this.activeOrder = - // 'e4a16aa0198022dc682b2b52ed15767438282c0e712f510332fc047eaf795313' - // await this.listenMessages() - // } }, created() { this.customerPubkey = this.account.pubkey From 02746ec170bf5ebea96ac8e124907af29374f6c0 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Wed, 8 Mar 2023 15:57:34 +0000 Subject: [PATCH 243/891] tweaks --- static/js/market.js | 2 +- templates/nostrmarket/market.html | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/static/js/market.js b/static/js/market.js index 0d823e9..3cf9b90 100644 --- a/static/js/market.js +++ b/static/js/market.js @@ -222,7 +222,6 @@ const market = async () => { }) }) await Promise.resolve(sub) - this.$q.loading.hide() this.stalls = await Array.from(stalls.values()) this.products = Array.from(products.values()).map(obj => { @@ -234,6 +233,7 @@ const market = async () => { } return obj }) + this.$q.loading.hide() pool.close(relays) return }, diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html index a5ebeb3..baa9715 100644 --- a/templates/nostrmarket/market.html +++ b/templates/nostrmarket/market.html @@ -57,7 +57,7 @@ {{ profiles.get(pub).name }} Date: Wed, 8 Mar 2023 15:57:34 +0000 Subject: [PATCH 244/891] tweaks --- static/js/market.js | 2 +- templates/nostrmarket/market.html | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/static/js/market.js b/static/js/market.js index 0d823e9..3cf9b90 100644 --- a/static/js/market.js +++ b/static/js/market.js @@ -222,7 +222,6 @@ const market = async () => { }) }) await Promise.resolve(sub) - this.$q.loading.hide() this.stalls = await Array.from(stalls.values()) this.products = Array.from(products.values()).map(obj => { @@ -234,6 +233,7 @@ const market = async () => { } return obj }) + this.$q.loading.hide() pool.close(relays) return }, diff --git a/templates/nostrmarket/market.html b/templates/nostrmarket/market.html index a5ebeb3..baa9715 100644 --- a/templates/nostrmarket/market.html +++ b/templates/nostrmarket/market.html @@ -57,7 +57,7 @@ {{ profiles.get(pub).name }} Date: Wed, 8 Mar 2023 21:05:53 +0000 Subject: [PATCH 245/891] brush up checkout dialog --- .../components/chat-dialog/chat-dialog.html | 1 - .../customer-stall/customer-stall.html | 94 +++++++++++++++---- .../customer-stall/customer-stall.js | 25 ++++- 3 files changed, 96 insertions(+), 24 deletions(-) diff --git a/static/components/chat-dialog/chat-dialog.html b/static/components/chat-dialog/chat-dialog.html index fd87273..1e46a4d 100644 --- a/static/components/chat-dialog/chat-dialog.html +++ b/static/components/chat-dialog/chat-dialog.html @@ -2,7 +2,6 @@
@@ -62,35 +62,77 @@ v-model.trim="checkoutDialog.data.username" label="Name *optional" > + + + + It seems you haven't logged in. You can: +
    +
  1. + enter your public and private keys bellow (to sign the order + message) +
  2. +
  3. use a Nostr Signer Extension (NIP07)
  4. +
  5. + fill out the required fields, without keys, and download the + order and send as a direct message to the merchant on any + Nostr client +
  6. +
+
+ + Use a Nostr browser extension + Download the order and send manually + +
+
+ -
Download Order + Checkout @@ -154,7 +208,7 @@ position="top" @hide="closeQrCodeDialog" > - +
diff --git a/static/components/customer-stall/customer-stall.js b/static/components/customer-stall/customer-stall.js index 58ab910..b417c91 100644 --- a/static/components/customer-stall/customer-stall.js +++ b/static/components/customer-stall/customer-stall.js @@ -16,6 +16,7 @@ async function customerStall(path) { data: function () { return { loading: false, + isPwd: true, cart: { total: 0, size: 0, @@ -113,6 +114,24 @@ async function customerStall(path) { products: new Map() } }, + async downloadOrder() { + return + }, + async getFromExtension() { + this.customerPubkey = await window.nostr.getPublicKey() + this.customerUseExtension = true + this.checkoutDialog.data.pubkey = this.customerPubkey + }, + openCheckout() { + // Check if user is logged in + if (this.customerPubkey) { + this.checkoutDialog.data.pubkey = this.customerPubkey + if (this.customerPrivkey && !useExtension) { + this.checkoutDialog.data.privkey = this.customerPrivkey + } + } + this.checkoutDialog.show = true + }, resetCheckout() { this.checkoutDialog = { show: false, @@ -296,9 +315,9 @@ async function customerStall(path) { } }, created() { - this.customerPubkey = this.account.pubkey - this.customerPrivkey = this.account.privkey - this.customerUseExtension = this.account.useExtension + this.customerPubkey = this.account?.pubkey + this.customerPrivkey = this.account?.privkey + this.customerUseExtension = this.account?.useExtension setTimeout(() => { if (window.nostr) { this.hasNip07 = true From 88a40f48a77291d0db58286c5f47b78d6f861098 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Wed, 8 Mar 2023 21:05:53 +0000 Subject: [PATCH 246/891] brush up checkout dialog --- .../components/chat-dialog/chat-dialog.html | 1 - .../customer-stall/customer-stall.html | 94 +++++++++++++++---- .../customer-stall/customer-stall.js | 25 ++++- 3 files changed, 96 insertions(+), 24 deletions(-) diff --git a/static/components/chat-dialog/chat-dialog.html b/static/components/chat-dialog/chat-dialog.html index fd87273..1e46a4d 100644 --- a/static/components/chat-dialog/chat-dialog.html +++ b/static/components/chat-dialog/chat-dialog.html @@ -2,7 +2,6 @@
@@ -62,35 +62,77 @@ v-model.trim="checkoutDialog.data.username" label="Name *optional" > + + + + It seems you haven't logged in. You can: +
    +
  1. + enter your public and private keys bellow (to sign the order + message) +
  2. +
  3. use a Nostr Signer Extension (NIP07)
  4. +
  5. + fill out the required fields, without keys, and download the + order and send as a direct message to the merchant on any + Nostr client +
  6. +
+
+ + Use a Nostr browser extension + Download the order and send manually + +
+
+ -
Download Order + Checkout @@ -154,7 +208,7 @@ position="top" @hide="closeQrCodeDialog" > - +
diff --git a/static/components/customer-stall/customer-stall.js b/static/components/customer-stall/customer-stall.js index 58ab910..b417c91 100644 --- a/static/components/customer-stall/customer-stall.js +++ b/static/components/customer-stall/customer-stall.js @@ -16,6 +16,7 @@ async function customerStall(path) { data: function () { return { loading: false, + isPwd: true, cart: { total: 0, size: 0, @@ -113,6 +114,24 @@ async function customerStall(path) { products: new Map() } }, + async downloadOrder() { + return + }, + async getFromExtension() { + this.customerPubkey = await window.nostr.getPublicKey() + this.customerUseExtension = true + this.checkoutDialog.data.pubkey = this.customerPubkey + }, + openCheckout() { + // Check if user is logged in + if (this.customerPubkey) { + this.checkoutDialog.data.pubkey = this.customerPubkey + if (this.customerPrivkey && !useExtension) { + this.checkoutDialog.data.privkey = this.customerPrivkey + } + } + this.checkoutDialog.show = true + }, resetCheckout() { this.checkoutDialog = { show: false, @@ -296,9 +315,9 @@ async function customerStall(path) { } }, created() { - this.customerPubkey = this.account.pubkey - this.customerPrivkey = this.account.privkey - this.customerUseExtension = this.account.useExtension + this.customerPubkey = this.account?.pubkey + this.customerPrivkey = this.account?.privkey + this.customerUseExtension = this.account?.useExtension setTimeout(() => { if (window.nostr) { this.hasNip07 = true From f9fc52ac524228120dd4d1ca21d22996f0c8bdf8 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Thu, 9 Mar 2023 10:00:55 +0000 Subject: [PATCH 247/891] fixing vlad's review comments --- .../components/chat-dialog/chat-dialog.html | 2 +- static/components/chat-dialog/chat-dialog.js | 57 +------------------ static/js/market.js | 9 +-- static/js/utils.js | 54 ++++++++++++++++++ views.py | 14 +---- 5 files changed, 63 insertions(+), 73 deletions(-) diff --git a/static/components/chat-dialog/chat-dialog.html b/static/components/chat-dialog/chat-dialog.html index 1e46a4d..a428044 100644 --- a/static/components/chat-dialog/chat-dialog.html +++ b/static/components/chat-dialog/chat-dialog.html @@ -33,7 +33,7 @@ :text="[message.msg]" :sent="message.sender == 'Me'" :bg-color="message.sender == 'Me' ? 'white' : 'light-green-2'" - :stamp="`${timeFromNow(message.timestamp * 1000)}`" + :stamp="message.timestamp" size="6" /> diff --git a/static/components/chat-dialog/chat-dialog.js b/static/components/chat-dialog/chat-dialog.js index e89e26d..c111e27 100644 --- a/static/components/chat-dialog/chat-dialog.js +++ b/static/components/chat-dialog/chat-dialog.js @@ -69,7 +69,7 @@ async function chatDialog(path) { } messagesMap.set(event.id, { msg: plaintext, - timestamp: event.created_at, + timestamp: timeFromNow(event.created_at * 1000), sender: `${mine ? 'Me' : 'Merchant'}` }) } catch { @@ -107,11 +107,9 @@ async function chatDialog(path) { let pub = relay.publish(event) pub.on('ok', () => { console.debug(`${relay.url} has accepted our event`) - relay.close() }) pub.on('failed', reason => { console.debug(`failed to publish to ${relay.url}: ${reason}`) - relay.close() }) this.newMessage = '' } catch (e) { @@ -146,59 +144,6 @@ async function chatDialog(path) { event = await window.nostr.signEvent(event) } return event - }, - timeFromNow(time) { - // Get timestamps - let unixTime = new Date(time).getTime() - if (!unixTime) return - let now = new Date().getTime() - - // Calculate difference - let difference = unixTime / 1000 - now / 1000 - - // Setup return object - let tfn = {} - - // Check if time is in the past, present, or future - tfn.when = 'now' - if (difference > 0) { - tfn.when = 'future' - } else if (difference < -1) { - tfn.when = 'past' - } - - // Convert difference to absolute - difference = Math.abs(difference) - - // Calculate time unit - if (difference / (60 * 60 * 24 * 365) > 1) { - // Years - tfn.unitOfTime = 'years' - tfn.time = Math.floor(difference / (60 * 60 * 24 * 365)) - } else if (difference / (60 * 60 * 24 * 45) > 1) { - // Months - tfn.unitOfTime = 'months' - tfn.time = Math.floor(difference / (60 * 60 * 24 * 45)) - } else if (difference / (60 * 60 * 24) > 1) { - // Days - tfn.unitOfTime = 'days' - tfn.time = Math.floor(difference / (60 * 60 * 24)) - } else if (difference / (60 * 60) > 1) { - // Hours - tfn.unitOfTime = 'hours' - tfn.time = Math.floor(difference / (60 * 60)) - } else if (difference / 60 > 1) { - // Minutes - tfn.unitOfTime = 'minutes' - tfn.time = Math.floor(difference / 60) - } else { - // Seconds - tfn.unitOfTime = 'seconds' - tfn.time = Math.floor(difference) - } - - // Return time from now data - return `${tfn.time} ${tfn.unitOfTime}` } }, created() { diff --git a/static/js/market.js b/static/js/market.js index 3cf9b90..52fed25 100644 --- a/static/js/market.js +++ b/static/js/market.js @@ -62,12 +62,13 @@ const market = async () => { if (this.activeStall) { products = products.filter(p => p.stall_id == this.activeStall) } - if (!this.searchText || this.searchText.length < 2) return products + const searchText = this.searchText.toLowerCase() + if (!searchText || searchText.length < 2) return products return products.filter(p => { return ( - p.name.includes(this.searchText) || - p.description.includes(this.searchText) || - p.categories.includes(this.searchText) + p.name.toLowerCase().includes(searchText) || + p.description.toLowerCase().includes(searchText) || + p.categories.toLowerCase().includes(searchText) ) }) }, diff --git a/static/js/utils.js b/static/js/utils.js index 736fc8d..86c4d00 100644 --- a/static/js/utils.js +++ b/static/js/utils.js @@ -47,3 +47,57 @@ function isJson(str) { return false } } + +function timeFromNow(time) { + // Get timestamps + let unixTime = new Date(time).getTime() + if (!unixTime) return + let now = new Date().getTime() + + // Calculate difference + let difference = unixTime / 1000 - now / 1000 + + // Setup return object + let tfn = {} + + // Check if time is in the past, present, or future + tfn.when = 'now' + if (difference > 0) { + tfn.when = 'future' + } else if (difference < -1) { + tfn.when = 'past' + } + + // Convert difference to absolute + difference = Math.abs(difference) + + // Calculate time unit + if (difference / (60 * 60 * 24 * 365) > 1) { + // Years + tfn.unitOfTime = 'years' + tfn.time = Math.floor(difference / (60 * 60 * 24 * 365)) + } else if (difference / (60 * 60 * 24 * 45) > 1) { + // Months + tfn.unitOfTime = 'months' + tfn.time = Math.floor(difference / (60 * 60 * 24 * 45)) + } else if (difference / (60 * 60 * 24) > 1) { + // Days + tfn.unitOfTime = 'days' + tfn.time = Math.floor(difference / (60 * 60 * 24)) + } else if (difference / (60 * 60) > 1) { + // Hours + tfn.unitOfTime = 'hours' + tfn.time = Math.floor(difference / (60 * 60)) + } else if (difference / 60 > 1) { + // Minutes + tfn.unitOfTime = 'minutes' + tfn.time = Math.floor(difference / 60) + } else { + // Seconds + tfn.unitOfTime = 'seconds' + tfn.time = Math.floor(difference) + } + + // Return time from now data + return `${tfn.time} ${tfn.unitOfTime}` +} diff --git a/views.py b/views.py index d83bc85..3b757fb 100644 --- a/views.py +++ b/views.py @@ -23,18 +23,8 @@ async def index(request: Request, user: User = Depends(check_user_exists)): @nostrmarket_ext.get("/market", response_class=HTMLResponse) -async def market( - request: Request, - stall_id: str = Query(None), - product_id: str = Query(None), - merchant_pubkey: str = Query(None), -): +async def market(request: Request): return nostrmarket_renderer().TemplateResponse( "nostrmarket/market.html", - { - "request": request, - "stall_id": stall_id, - "product_id": product_id, - "merchant_pubkey": merchant_pubkey, - }, + {"request": request}, ) From bed734e9b424f80d452a53bca912913a31a52ece Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Thu, 9 Mar 2023 10:00:55 +0000 Subject: [PATCH 248/891] fixing vlad's review comments --- .../components/chat-dialog/chat-dialog.html | 2 +- static/components/chat-dialog/chat-dialog.js | 57 +------------------ static/js/market.js | 9 +-- static/js/utils.js | 54 ++++++++++++++++++ views.py | 14 +---- 5 files changed, 63 insertions(+), 73 deletions(-) diff --git a/static/components/chat-dialog/chat-dialog.html b/static/components/chat-dialog/chat-dialog.html index 1e46a4d..a428044 100644 --- a/static/components/chat-dialog/chat-dialog.html +++ b/static/components/chat-dialog/chat-dialog.html @@ -33,7 +33,7 @@ :text="[message.msg]" :sent="message.sender == 'Me'" :bg-color="message.sender == 'Me' ? 'white' : 'light-green-2'" - :stamp="`${timeFromNow(message.timestamp * 1000)}`" + :stamp="message.timestamp" size="6" /> diff --git a/static/components/chat-dialog/chat-dialog.js b/static/components/chat-dialog/chat-dialog.js index e89e26d..c111e27 100644 --- a/static/components/chat-dialog/chat-dialog.js +++ b/static/components/chat-dialog/chat-dialog.js @@ -69,7 +69,7 @@ async function chatDialog(path) { } messagesMap.set(event.id, { msg: plaintext, - timestamp: event.created_at, + timestamp: timeFromNow(event.created_at * 1000), sender: `${mine ? 'Me' : 'Merchant'}` }) } catch { @@ -107,11 +107,9 @@ async function chatDialog(path) { let pub = relay.publish(event) pub.on('ok', () => { console.debug(`${relay.url} has accepted our event`) - relay.close() }) pub.on('failed', reason => { console.debug(`failed to publish to ${relay.url}: ${reason}`) - relay.close() }) this.newMessage = '' } catch (e) { @@ -146,59 +144,6 @@ async function chatDialog(path) { event = await window.nostr.signEvent(event) } return event - }, - timeFromNow(time) { - // Get timestamps - let unixTime = new Date(time).getTime() - if (!unixTime) return - let now = new Date().getTime() - - // Calculate difference - let difference = unixTime / 1000 - now / 1000 - - // Setup return object - let tfn = {} - - // Check if time is in the past, present, or future - tfn.when = 'now' - if (difference > 0) { - tfn.when = 'future' - } else if (difference < -1) { - tfn.when = 'past' - } - - // Convert difference to absolute - difference = Math.abs(difference) - - // Calculate time unit - if (difference / (60 * 60 * 24 * 365) > 1) { - // Years - tfn.unitOfTime = 'years' - tfn.time = Math.floor(difference / (60 * 60 * 24 * 365)) - } else if (difference / (60 * 60 * 24 * 45) > 1) { - // Months - tfn.unitOfTime = 'months' - tfn.time = Math.floor(difference / (60 * 60 * 24 * 45)) - } else if (difference / (60 * 60 * 24) > 1) { - // Days - tfn.unitOfTime = 'days' - tfn.time = Math.floor(difference / (60 * 60 * 24)) - } else if (difference / (60 * 60) > 1) { - // Hours - tfn.unitOfTime = 'hours' - tfn.time = Math.floor(difference / (60 * 60)) - } else if (difference / 60 > 1) { - // Minutes - tfn.unitOfTime = 'minutes' - tfn.time = Math.floor(difference / 60) - } else { - // Seconds - tfn.unitOfTime = 'seconds' - tfn.time = Math.floor(difference) - } - - // Return time from now data - return `${tfn.time} ${tfn.unitOfTime}` } }, created() { diff --git a/static/js/market.js b/static/js/market.js index 3cf9b90..52fed25 100644 --- a/static/js/market.js +++ b/static/js/market.js @@ -62,12 +62,13 @@ const market = async () => { if (this.activeStall) { products = products.filter(p => p.stall_id == this.activeStall) } - if (!this.searchText || this.searchText.length < 2) return products + const searchText = this.searchText.toLowerCase() + if (!searchText || searchText.length < 2) return products return products.filter(p => { return ( - p.name.includes(this.searchText) || - p.description.includes(this.searchText) || - p.categories.includes(this.searchText) + p.name.toLowerCase().includes(searchText) || + p.description.toLowerCase().includes(searchText) || + p.categories.toLowerCase().includes(searchText) ) }) }, diff --git a/static/js/utils.js b/static/js/utils.js index 736fc8d..86c4d00 100644 --- a/static/js/utils.js +++ b/static/js/utils.js @@ -47,3 +47,57 @@ function isJson(str) { return false } } + +function timeFromNow(time) { + // Get timestamps + let unixTime = new Date(time).getTime() + if (!unixTime) return + let now = new Date().getTime() + + // Calculate difference + let difference = unixTime / 1000 - now / 1000 + + // Setup return object + let tfn = {} + + // Check if time is in the past, present, or future + tfn.when = 'now' + if (difference > 0) { + tfn.when = 'future' + } else if (difference < -1) { + tfn.when = 'past' + } + + // Convert difference to absolute + difference = Math.abs(difference) + + // Calculate time unit + if (difference / (60 * 60 * 24 * 365) > 1) { + // Years + tfn.unitOfTime = 'years' + tfn.time = Math.floor(difference / (60 * 60 * 24 * 365)) + } else if (difference / (60 * 60 * 24 * 45) > 1) { + // Months + tfn.unitOfTime = 'months' + tfn.time = Math.floor(difference / (60 * 60 * 24 * 45)) + } else if (difference / (60 * 60 * 24) > 1) { + // Days + tfn.unitOfTime = 'days' + tfn.time = Math.floor(difference / (60 * 60 * 24)) + } else if (difference / (60 * 60) > 1) { + // Hours + tfn.unitOfTime = 'hours' + tfn.time = Math.floor(difference / (60 * 60)) + } else if (difference / 60 > 1) { + // Minutes + tfn.unitOfTime = 'minutes' + tfn.time = Math.floor(difference / 60) + } else { + // Seconds + tfn.unitOfTime = 'seconds' + tfn.time = Math.floor(difference) + } + + // Return time from now data + return `${tfn.time} ${tfn.unitOfTime}` +} diff --git a/views.py b/views.py index d83bc85..3b757fb 100644 --- a/views.py +++ b/views.py @@ -23,18 +23,8 @@ async def index(request: Request, user: User = Depends(check_user_exists)): @nostrmarket_ext.get("/market", response_class=HTMLResponse) -async def market( - request: Request, - stall_id: str = Query(None), - product_id: str = Query(None), - merchant_pubkey: str = Query(None), -): +async def market(request: Request): return nostrmarket_renderer().TemplateResponse( "nostrmarket/market.html", - { - "request": request, - "stall_id": stall_id, - "product_id": product_id, - "merchant_pubkey": merchant_pubkey, - }, + {"request": request}, ) From d7dae2be8b5622f1abb69e110d53e2bd669465a0 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Thu, 9 Mar 2023 10:23:33 +0000 Subject: [PATCH 249/891] Auto stash before rebase of "main" --- .../customer-stall/customer-stall.html | 22 ++++++++++++++- .../customer-stall/customer-stall.js | 28 +++++++++++++++++-- 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/static/components/customer-stall/customer-stall.html b/static/components/customer-stall/customer-stall.html index d6afaba..572b90e 100644 --- a/static/components/customer-stall/customer-stall.html +++ b/static/components/customer-stall/customer-stall.html @@ -123,7 +123,8 @@ :type="isPwd ? 'password' : 'text'" v-if="!customerUseExtension" v-model.trim="checkoutDialog.data.privkey" - hint="Enter your private key or see bellow for instructions" + label="Private key" + hint="Enter your private key" >